PageSourceSearch

https://customer.nationalgrid.co.uk/public/javascripts/angular.wpd-accessibility.js

js nationalgrid.co.uk collected 2026-09-24 09:02:12 UTC 26,691 bytes, 687 lines download raw bytes

1/**
2 * @author noahm
3 */
4var accessibility = angular.module('wpd-accessibility', []);
5
6//class SelfResetRegex  {
7//    constructor(reg) {
8//        this.reg = reg;
9//    }
10//    exec(str) {
11//        var result = this.reg.exec(str);
12//        this.reg.lastIndex = 0;
13//        return result;
14//    }
15//}
16//
17//class WPDColorObject {
18//    constructor(R, G, B, A) {
19//        this.R = R;
20//        this.G = G;
21//        this.B = B;
22//        this.A = A;
23//    }
24//}
25
26/**
27 * @constructor
28 * @param {RegExp} reg
29 * @returns {SelfResetRegex}
30 */
31function SelfResetRegex(reg) {
32    this.reg = reg;
33};
34SelfResetRegex.prototype.exec = function(str) {
35    var result = this.reg.exec(str);
36    this.reg.lastIndex = 0;
37    return result;
38};
39
40/**
41 * @constructor
42 * @param {Number} R  (0-255)
43 * @param {Number} G
44 * @param {Number} B
45 * @param {Number} A  (0.0-1.0)
46 * @returns {WPDColorObject}
47 */
48function WPDColorObject(R, G, B, A) {
49    this.R = R;
50    this.G = G;
51    this.B = B;
52    this.A = A;
53};
54
55accessibility.constant('accessibilityKeyCodes', {
56   'ENTER'          : 13,
57   'SPACE'          : 32,
58   'LEFT_ARROW'     : 37,
59   'UP_ARROW'       : 38,
60   'RIGHT_ARROW'    : 39,
61   'DOWN_ARROW'     : 40
62});
63accessibility.constant('baseFocusableElements', [
64    'button',
65    'a',
66    '.focusable-element',
67    'select',
68    'input',
69    'textarea'
70]);
71accessibility.constant('baseFocusableExceptions', [
72   'a.skiptocontent'
73]);
74accessibility.constant('focusVariables', {
75    transparencyThreshold: 0.1,                                     //anything <= this consider to be transparent
76    opacityThreshold: 0.9,                                          //anything >= this consider to be opaque
77    blendLevels: 2,                                                 //how deep should recursion go to blend with parent(s)
78    defaultDarkFocus: (new WPDColorObject(48, 124, 127, 1)),        // #307c7f green
79    defaultLightFocus: (new WPDColorObject(168, 229, 240, 1)),      // #a8e5f0 cold-blue
80    transparentObj: {effectiveColor: (new WPDColorObject(255, 255, 255, 0))},
81    whiteObj: {effectiveColor: (new WPDColorObject(255, 255, 255, 1))},
82    minimumContrastReqLarge: 3,                                     // https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
83    minimumContrastReqSmall: 4.5,
84    textBoldSize: 18.5,
85    textLargeSize: 24,
86    rgbaRegex: (new SelfResetRegex(/^rgba?\(([0-9]{1,3})\s?,\s?([0-9]{1,3})\s?,\s?([0-9]{1,3})\s?(,\s?([0-9]{1,3}\.?[0-9]*))?\)$/gi)),
87    hexaRegex: (new SelfResetRegex(/^#?([0-9A-Fa-f]{6}|[0-9A-Fa-f]{3})([0-9A-Fa-f]{2})?$/gi))         // this one assumes that it won't see an invalid hex value with the format RGBaa (using hexa means you need RRGGBBaa)
88
89});
90
91/**
92 * Inject accessibilitySettingsProvider into your config() function to make setting changes.
93 *
94 * NOTE: If your focusColors in your CSS differ from the default ones above, you need to set them to match your CSS values,
95 *  otherwise this script will be comparing luminance for the wrong colors!
96 */
97accessibility.provider('accessibilitySettings', ['focusVariables', function(focusVariables) {
98        var self = this;
99
100        self.focusableElements = [];
101        self.focusableExceptions = [];
102        self.darkFocus = focusVariables.defaultDarkFocus;
103        self.lightFocus = focusVariables.defaultLightFocus;
104
105        /**
106         *
107         * @param {String[] | String} what
108         * @returns {undefined}
109         */
110        self.addFocusableElement = function(what) {
111            if (typeof what === 'string') {
112                this.focusableElements.push(what);
113            }
114            else if (Array.isArray(what)) {
115                for(var i = 0; i < what.length; i++) {
116                    if (typeof what[i] === 'string') {
117                        this.focusableElements.push(what[i]);
118                    }
119                }
120            }
121        };
122
123        /**
124         *
125         * @param {String[] | String} what
126         * @returns {undefined}
127         */
128        self.addFocusableException = function(what) {
129            if (typeof what === 'string') {
130                this.focusableExceptions.push(what);
131            }
132            else if (Array.isArray(what)) {
133                for(var i = 0; i < what.length; i++) {
134                    if (typeof what[i] === 'string') {
135                        this.focusableExceptions.push(what[i]);
136                    }
137                }
138            }
139        };
140
141        /**
142         *
143         * @param {Number} r
144         * @param {Number} g
145         * @param {Number} b
146         * @param {Number} a
147         * @returns {undefined}
148         */
149        self.setLightFocus = function(r,g,b,a) {
150            this.lightFocus = new WPDColorObject(r,g,b,a);
151        };
152
153        /**
154         *
155         * @param {Number} r
156         * @param {Number} g
157         * @param {Number} b
158         * @param {Number} a
159         * @returns {undefined}
160         */
161        self.setDarkFocus = function(r,g,b,a) {
162            this.darkFocus = new WPDColorObject(r,g,b,a);
163        };
164
165        // Service object (not available for use in config() functions)
166        this.$get = [
167            'baseFocusableElements',
168            'baseFocusableExceptions',
169            function(baseFocusableElements, baseFocusableExceptions) {
170                return {
171                    getFocusableElements: function() {
172                        return baseFocusableElements.concat(self.focusableElements);
173                    },
174                    getFocusableExceptions: function() {
175                        return baseFocusableExceptions.concat(self.focusableExceptions);
176                    },
177                    getLightFocusColor: function() {
178                        return self.lightFocus;
179                    },
180                    getDarkFocusColor: function() {
181                        return self.darkFocus;
182                    }
183                };
184            }
185        ];
186    }]
187);
188
189// [WSME-590]
190/**
191 * @author noahm
192 */
193accessibility.service('colorService', ['focusVariables', function(focusVariables){
194    var service = {};
195
196    /**
197     * Take a hex or 0-255 and determine its worth in a 0-1 range
198     * @param {String | Number} value
199     * @returns {null | Number}
200     */
201    service.normaliseValue = function(value) {
202        if (value !== null && value !== undefined) {
203            var isHex = /^[0-9a-fA-f]+$/gi;
204            if (isHex.test(value)) {
205                value = parseInt('0x'+value);
206            }
207            if (!isNaN(parseFloat(value))) {
208                var max = 255;
209                var min = 0;
210                var value = parseFloat(value);
211
212                return (value - min) / (max - min);
213            }
214        }
215        return null;
216    };
217
218    /**
219     *
220     * @param {Number} alpha
221     * @param {Number} col1
222     * @param {Number} col2
223     * @returns {Number}
224     */
225    service.blendColorValue = function(alpha, col1, col2) {
226        alpha = parseFloat(alpha);
227        col1 = parseFloat(col1);
228        col2 = parseFloat(col2);
229        return (alpha * col1) + ((1-alpha) * col2);
230    };
231
232    /**
233     *
234     * @param {WPDColorObject} back
235     * @param {WPDColorObject} fore
236     * @returns {WPDColorObject}
237     */
238    service.blendColorObj = function(back, fore) {
239        return new WPDColorObject(
240                service.blendColorValue(fore.A, fore.R, back.R),
241                service.blendColorValue(fore.A, fore.G, back.G),
242                service.blendColorValue(fore.A, fore.B, back.B),
243                (parseFloat(fore.A) + (parseFloat(back.A) * (1-parseFloat(fore.A))))
244            );
245    };
246
247
248    /**
249     * This is not foolproof! It can't take into consideration background images, or the visual contents of img elements.
250     *
251     * Returns null if the element is deemed to be invisible/transparent according to display, visibility, opacity and bgColor alpha value (if below threshold)
252     * Colours above opacityThreahold are returned as-is.
253     * Colours deemed semi-transparent will attempt to blend with their parent's colours (up to blendLevel times) to get an approximate colour of what the user is actually seeing on screen.
254     *      This action is recursive, and will terminate when the blendDepth is reached, or the document object has been reached.
255     *
256     *  The returned object's effectiveColour.A value is for the purpose of blending within this function.
257     *      For all intents and purposes the alpha of the ultimately returned effective colour should always be considered 1.
258     *
259     * @param {jQuery} $elem
260     * @param {Number} blendDepth
261     * @returns {null | Object}
262     */
263    service.getEffectiveColor = function($elem, blendDepth) {
264        //console.log($elem);
265
266        if ($elem[0] === document) return null;
267
268        if (blendDepth === undefined || blendDepth === null || blendDepth > focusVariables.blendLevels) {
269            blendDepth = focusVariables.blendLevels;
270        }
271        //console.log('blendDepth: '+blendDepth);
272
273        var display = $elem.css('display');
274        if (display === 'none') {
275            return null;
276        }
277
278        var visibility = $elem.css('visibility');
279        if (visibility === 'hidden') {
280            return null;
281        }
282
283        // Technically I should be doing the blending thing with this value too, but considering this opacity change will propagate to any text inside the element,
284        // I'm making the assumption that it isn't supposed to be focusable anyway.
285        var opacity = $elem.css('opacity');
286        if (opacity !== '' && opacity <= focusVariables.transparencyThreshold) {
287            return null;
288        }
289
290        var bgColor = $elem.css('background-color');
291        //console.log(bgColor);
292        if (bgColor === null || bgColor === undefined || bgColor === '' || bgColor === 'transparent') {
293            return null;
294        }
295
296        var rgbaMatches = focusVariables.rgbaRegex.exec(bgColor);
297        var hexaMatches = focusVariables.hexaRegex.exec(bgColor);
298
299        var bgColorAlpha = 1;
300        if (rgbaMatches && rgbaMatches.length > 1) {
301            if (rgbaMatches[5] !== null && rgbaMatches[5] !== undefined) bgColorAlpha = rgbaMatches[5];     //apparently the rgba value .css() returns normalises the alpha value by itself.
302        }
303        else if (hexaMatches && hexaMatches.length > 1) {
304            var x = service.normaliseValue(hexaMatches[2]);
305            if (x !== null) bgColorAlpha = x;
306        }
307
308        if (bgColorAlpha !== null && bgColorAlpha !== undefined && bgColorAlpha !== '') {
309            if (bgColorAlpha <= focusVariables.transparencyThreshold) {
310                return null;
311            }
312        }
313
314        var trueColorObj = null;
315        if (rgbaMatches && rgbaMatches.length > 1) {
316            trueColorObj = new WPDColorObject(
317                    (rgbaMatches[1] || 0),
318                    (rgbaMatches[2] || 0),
319                    (rgbaMatches[3] || 0),
320                    bgColorAlpha
321                );
322        }
323        else if (hexaMatches && hexaMatches.length > 1) {
324            if (hexaMatches[1] !== null && hexaMatches[1] !== undefined) {
325                if (hexaMatches[1].length === 3) {
326                    trueColorObj = new WPDColorObject(
327                        (parseInt('0x' + hexaMatches[1][0] + hexaMatches[1][0]) || 0),
328                        (parseInt('0x' + hexaMatches[1][1] + hexaMatches[1][1]) || 0),
329                        (parseInt('0x' + hexaMatches[1][2] + hexaMatches[1][2]) || 0),
330                        bgColorAlpha
331                    );
332                }
333                else if (hexaMatches[1].length === 6) {
334                    trueColorObj = new WPDColorObject(
335                        (parseInt('0x' + hexaMatches[1][0] + hexaMatches[1][1]) || 0),
336                        (parseInt('0x' + hexaMatches[1][2] + hexaMatches[1][3]) || 0),
337                        (parseInt('0x' + hexaMatches[1][4] + hexaMatches[1][5]) || 0),
338                        bgColorAlpha
339                    );
340                }
341            }
342        }
343
344        if (trueColorObj === null) return null;
345
346        var effectiveColor = trueColorObj;
347
348        //start blending with parent bg Colors if this isn't deemed to be opaque enough.
349        if (bgColorAlpha < focusVariables.opacityThreshold && $elem.parent()[0] !== document && blendDepth > 0) {
350            //console.log('blend with parent');
351            var $parent = $elem.parent();
352            //console.log($parent);
353            var parentCol;
354            //attempt to skip over transparent parents.
355            while (true) {
356                if ($parent[0] === document) {
357                    parentCol = focusVariables.whiteObj;
358                    break;
359                }
360                parentCol = service.getEffectiveColor($parent, blendDepth - 1); //recurse
361                if (parentCol) break;
362                //console.log('parent was transparent, try again for depth ' + blendDepth );
363                $parent = $parent.parent();
364                //console.log($parent);
365            }
366
367            parentCol = parentCol || focusVariables.transparentObj;
368            if (parentCol) {
369                effectiveColor = service.blendColorObj(parentCol.effectiveColor, trueColorObj);
370            }
371        }
372
373       // console.log('returned effectiveColor for depth '+blendDepth);
374        //console.log(effectiveColor);
375
376        return {
377            trueColor: bgColor,
378            trueColorObj: trueColorObj,
379            effectiveColor: effectiveColor
380        };
381    };
382
383    /**
384     * Traverse up parents and return the first *effectively determined* background-colour that isn't transparent.
385     * If all parents are transparent, you'll eventually hit the document object, which will return white as its bgColor.
386     *
387     * Again, not foolproof! This assumes that the elements have static positioning and are visually nested in their containers.
388     *
389     * @param {jQuery} $elem
390     * @param {boolean} includingElem
391     * @returns {WPDColorObj}
392     */
393    service.determineBgColor = function($elem, includingElem) {
394        if (includingElem) {
395            var elemCol = service.getEffectiveColor($elem);
396            if (elemCol) {
397                return elemCol;
398            }
399        }
400        var $parent = $elem.parent();
401        while (true) {
402            if ($parent[0] === document) return focusVariables.whiteObj;
403            var parentCol = service.getEffectiveColor($parent);
404            if (parentCol) {
405                return parentCol;
406            }
407            $parent = $parent.parent();
408        }
409    };
410
411
412    /**
413     * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#relativeluminancedef
414     *
415     * Calculate the luminance value of an rgb colour, according to the definition in the link above.
416     *
417     * @param {WPDColorObj} colorObj
418     * @returns {Number}
419     */
420    service.determineLuminance = function(colorObj) {
421        var r, g, b;
422        var srgbColObj = {
423            R: colorObj.R / 255,
424            G: colorObj.G / 255,
425            B: colorObj.B / 255,
426            calculate: function(what) {
427                if (what <= 0.03928) {
428                    return what / 12.92;
429                }
430                else {
431                    return Math.pow(((what + 0.055) / 1.055), 2.4);
432                }
433            }
434        };
435
436        r = srgbColObj.calculate(srgbColObj.R);
437        g = srgbColObj.calculate(srgbColObj.G);
438        b = srgbColObj.calculate(srgbColObj.B);
439
440        return (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
441    };
442
443    /**
444     * https://www.w3.org/TR/2008/REC-WCAG20-20081211/#contrast-ratiodef
445     *
446     * Calculate the contrast ratio between two luminance values, accordiong to the definition in the link above.
447     *
448     * @param {Number} luminance1
449     * @param {Number} luminance2
450     * @returns {Number}
451     */
452    service.contrastRatio = function(luminance1, luminance2) {
453        var lighter = Math.max(luminance1, luminance2);
454        var darker = Math.min(luminance1, luminance2);
455        return (lighter + 0.05) / (darker + 0.05);
456    };
457
458    return service;
459}]);
460
461accessibility.run([
462    'colorService',
463    'accessibilitySettings',
464    'focusVariables',
465    'DOMObserver',
466    '$timeout',
467    function(colorService, accessibilitySettings, focusVariables, DOMObserver, $timeout) {
468        var lightFocusLum = colorService.determineLuminance(accessibilitySettings.getLightFocusColor());
469        var darkFocusLum = colorService.determineLuminance(accessibilitySettings.getDarkFocusColor());
470        var whiteLum = 1;
471        var blackLum = 0;
472
473         /**
474         *
475         * @param {jQuery} $elements
476         * @returns {undefined}
477         */
478        var setupEventListeners = function($elements) {
479            $elements.each(function(idx, $elem) {
480                $elem = angular.element($elem);
481                $elem.on('focus', function(){
482                    var backgroundColor = colorService.determineBgColor($elem);
483                    //console.log(backgroundColor);
484                    var luminance = colorService.determineLuminance(backgroundColor.effectiveColor);
485
486                    var contrastWithLight = colorService.contrastRatio(luminance, lightFocusLum);
487                    var contrastWithDark = colorService.contrastRatio(luminance, darkFocusLum);
488
489                    //console.log(contrastWithLight);
490                    //console.log(contrastWithDark);
491
492                    // https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
493                    var finalContrastReq = (parseFloat($elem.css('font-size')) >= focusVariables.textLargeSize ||
494                                                ((parseFloat($elem.css('font-size')) >= focusVariables.textBoldSize) &&
495                                                    ($elem.css('font-weight') === 'bold' ||
496                                                    $elem.css('font-weight') === 'bolder' ||
497                                                    $elem.css('font-weight') === '700' ||
498                                                    $elem.css('font-weight') === '800' ||
499                                                    $elem.css('font-weight') === '900')
500                                                )
501                                            ) ? focusVariables.minimumContrastReqLarge : focusVariables.minimumContrastReqSmall;
502                    var finalClass = 'focusable-element-';
503
504                   // console.log(finalContrastReq);
505
506                    //light is passes, dark doesn't
507                    if (contrastWithLight >= finalContrastReq && contrastWithDark < finalContrastReq) finalClass += 'light';
508                    //dark passes, light doesn't
509                    else if (contrastWithDark >= finalContrastReq && contrastWithLight < finalContrastReq) finalClass += 'dark';
510                    //both pass, so pick the better of the two
511                    else if (contrastWithLight >= finalContrastReq && contrastWithDark >= finalContrastReq) {
512                        finalClass += (contrastWithLight >= contrastWithDark ? 'light' : 'dark');
513                    }
514                    //neither custom colours cut it so go straight to either black or white outline.
515                    else {
516                        contrastWithLight = colorService.contrastRatio(luminance, whiteLum);
517                        contrastWithDark = colorService.contrastRatio(luminance, blackLum);
518
519                        finalClass += (contrastWithLight >= contrastWithDark ? 'white' : 'black');
520                    }
521                    $elem.removeClass('focusable-element-light', 'focusable-element-dark', 'focusable-element-white', 'focusable-element-black');
522                    $elem.addClass(finalClass);
523                });
524                $elem.on('blur', function() {
525                    $elem.removeClass('focusable-element-light', 'focusable-element-dark', 'focusable-element-white', 'focusable-element-black');
526                });
527            });
528        };
529
530        $timeout(function() {
531            //console.log('accessibility run!');
532            // it might have been nicer to watch document.activeElement, but apparently this doesn't get assigned for anything other than text type inputs for iOS.
533            var $allFocusableElements = angular.element(document).find(accessibilitySettings.getFocusableElements().join(', ')).not(accessibilitySettings.getFocusableExceptions().join(', '));
534            //console.log($allFocusableElements);
535            setupEventListeners($allFocusableElements);
536
537            //setup something to add more listeners for focusable elements added to the DOM later on.
538            DOMObserver.addObserver(document, function(mutations) {
539                for(var i = 0; i < mutations.length; i ++) {
540                    if (mutations[i].addedNodes.length) {
541                        var $newFocusables = angular.element(Array.prototype.slice.call(mutations[i].addedNodes).filter(function(element) {
542                            return element.nodeType === 1;
543                        })).find(accessibilitySettings.getFocusableElements().join(', ')).not(accessibilitySettings.getFocusableExceptions().join(', '));
544                        //console.log($newFocusables);
545                        setupEventListeners($newFocusables);
546                    }
547                }
548            });
549        });
550    }
551]);
552
553
554//https://stackoverflow.com/questions/3219758/detect-changes-in-the-dom
555accessibility.service('DOMObserver', ['$timeout', function($timeout) {
556    var MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
557
558    this.addObserver = function(obj, callback) {
559        if( !obj || (obj.nodeType !== 1 && obj.nodeType !== 9)) return null;    //allow Elements and Document
560
561        if (MutationObserver) {
562            var observer = new MutationObserver(function(mutations, observer){
563                $timeout(function() {
564                    callback(mutations);
565                });
566            });
567            // have the observer observe obj for changes in children
568            observer.observe( obj, { childList:true, subtree:true });
569        }
570        else if(window.addEventListener){
571            //console.log('window.addeventListener');
572            var f = function(mutations){
573                $timeout(function() {
574                    callback(mutations);
575                });
576            };
577            obj.addEventListener('DOMNodeInserted', f, false);
578            obj.addEventListener('DOMNodeRemoved', f, false);
579        }
580    };
581}]);
582
583
584accessibility.controller('MaxLengthNotice', [
585    '$scope',
586    '$element',
587    '$timeout',
588    function($scope, $element, $timeout) {
589        var mln = this;
590        var timeoutQueued = false;
591        var timeoutValue = 1000;
592
593        mln.setAriaLive = function(remainingCount, $message) {
594            if (remainingCount > mln.politeThreshold) {
595                $message.attr('aria-live', 'off');
596
597                if (timeoutValue !== null && timeoutValue !== undefined) {
598                    timeoutValue = 1000;
599                }
600            }
601            else {
602                if (remainingCount > mln.assertiveThreshold) {
603                    $message.attr('aria-live', 'polite');
604
605                    if (timeoutValue !== null && timeoutValue !== undefined) {
606
607                        if (remainingCount > mln.assertiveThreshold + 50) {
608                            timeoutValue = 500;
609                        }
610                        else {
611                            timeoutValue = 200;
612                        }
613                    }
614                }
615                else {
616                    $message.attr('aria-live', 'assertive');
617
618                    if (timeoutValue !== null && timeoutValue !== undefined) {
619                        timeoutValue = 0;
620                    }
621                }
622            }
623        };
624
625        mln.init = function() {
626            // we have to wait until our template is rendered (how $timeout works here I have no clue)
627            $timeout(function() {
628                var $message = $element.find('[data-mln-message]');
629                var $messageTagon = $('<span data-mln-refresh-message></span>');
630                $message.append($messageTagon);
631                var $textField = $element.find('[data-mln-field]');
632                var maxCharacters = $textField.attr('maxlength');
633                var toggleTagon = false;
634
635                if (mln.makeAtomic) {
636                    $message.attr('aria-atomic','true');
637                }
638
639                //initally get textfield char limit and set aria-live
640                mln.setAriaLive(maxCharacters - $textField.val().length, $message);
641
642                var remainingCount = maxCharacters - $textField.val().length;
643
644                $textField.on('keyup', function() {
645                    //don't spam evaluating this on every input event, so use timeouts
646                    // let us know if we already have one queued so we don't end up stacking them.
647                    if (!timeoutQueued) {
648                        $timeout(function() {
649                            remainingCount = maxCharacters - $textField.val().length;
650
651                            mln.setAriaLive(remainingCount, $message);
652
653                            // reiterate message if they keep trying to type once limit is reached
654                            if (remainingCount <= 0) {
655                                toggleTagon = !toggleTagon;
656                                $messageTagon.html(toggleTagon ? '': '&nbsp;');      // This is enough to get the screenreader to repeat the outer message.
657                            }                                                   // if you're wondering why we can't just tale and repaste the .text()
658                                                                                // of the $message, it's because we'd lose the angular expression generating it.
659
660
661                            timeoutQueued = false;
662                        }, timeoutValue);
663
664                        timeoutQueued = true;
665                    }
666                });
667            });
668        };
669    }
670]);
671
672accessibility.directive('maxLengthNotice', [
673    function() {
674        return {
675            scope: {
676                makeAtomic: '<',
677                politeThreshold: '<',
678                assertiveThreshold: '<'
679            },
680            transclude: true,
681            bindToController: true,
682            controllerAs: 'mln',
683            controller: 'MaxLengthNotice',
684            template: '<div ng-init="mln.init()"><ng-transclude></ng-transclude></div>'
685        };
686    }
687]);

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.