From b6e876876718d754288b415b8647ff89e803a932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Wed, 13 Mar 2019 15:51:12 +0000 Subject: [PATCH 01/59] Update dependencies (#280) * Remove an always failing test * Update dependencies * Revert 'temporary' npm package to v0.0.8 * Update Node version in Vagrantfile --- .gitignore | 2 +- bin/cli.js | 2 +- front/src/css/screenshot.css | 2 +- lib/tools/redownload/fileMinifier.js | 118 ++------------------------- package.json | 87 ++++++++++---------- server_config/server_install.sh | 2 +- test/core/phantomasWrapperTest.js | 32 -------- 7 files changed, 54 insertions(+), 191 deletions(-) diff --git a/.gitignore b/.gitignore index 19cd53d..242c403 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,8 @@ node_modules +package-lock.json .tmp .vagrant results/* coverage front/build - har.json \ No newline at end of file diff --git a/bin/cli.js b/bin/cli.js index a76b83d..cacb0f8 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -26,7 +26,7 @@ var cli = meow({ ' --reporter The output format: "json" or "xml". Default is "json".', '' ].join('\n'), - pkg: '../package.json' + pkg: require('../package.json') }); diff --git a/front/src/css/screenshot.css b/front/src/css/screenshot.css index 55af543..505d893 100644 --- a/front/src/css/screenshot.css +++ b/front/src/css/screenshot.css @@ -2,6 +2,6 @@ text-align: center; } .screenshot .screenshotWrapper { - font-size: 2.08333333333333em; + font-size: 2.08333333em; margin-bottom: 0.5em; } diff --git a/lib/tools/redownload/fileMinifier.js b/lib/tools/redownload/fileMinifier.js index 4f9371a..d7f86a3 100644 --- a/lib/tools/redownload/fileMinifier.js +++ b/lib/tools/redownload/fileMinifier.js @@ -141,123 +141,19 @@ var FileMinifier = function() { // Uglify function minifyJs(body) { - // Splitting the Uglify function because it sometime takes too long (more than 10 seconds) - // I hope that, by splitting, it can be a little more asynchronous, so the application doesn't freeze. + var deferred = Q.defer(); + var startTime = Date.now(); - return splittedUglifyStep1(body) - .delay(1) - .then(splittedUglifyStep2) - .delay(1) - .then(function(ast) { + var result = UglifyJS.minify(body, { // Only do the compression step for smaller files // otherwise it can take a very long time compared to the gain - if (body.length < 200*1024) { - return splittedUglifyStep3(ast); - } else { - debug('Skipping step 3 because the file is too big (%d bytes)!', body.length); - return ast; - } - }) - .delay(1) - .then(splittedUglifyStep4) - .delay(1) - .then(splittedUglifyStep5) - .delay(1) - .then(splittedUglifyStep6) - .delay(1) - .then(splittedUglifyStep7); - - } - - function splittedUglifyStep1(code) { - var deferred = Q.defer(); - var startTime = Date.now(); - - try { - var toplevel_ast = UglifyJS.parse(code); - - var endTime = Date.now(); - debug('Uglify step 1 took %dms', endTime - startTime); - deferred.resolve(toplevel_ast); - - } catch(err) { - debug('JS syntax error, Uglify\'s parser failed (step 1)'); - deferred.reject(err); - } - - return deferred.promise; - } - - function splittedUglifyStep2(toplevel) { - var deferred = Q.defer(); - var startTime = Date.now(); - - toplevel.figure_out_scope(); + compress: (body.length < 200*1024) + }); var endTime = Date.now(); - debug('Uglify step 2 took %dms', endTime - startTime); - deferred.resolve(toplevel); - return deferred.promise; - } + debug('Uglify took %dms', endTime - startTime); + deferred.resolve(result.code); - function splittedUglifyStep3(toplevel) { - var deferred = Q.defer(); - var startTime = Date.now(); - - var compressor = UglifyJS.Compressor({warnings: false}); - var compressed_ast = toplevel.transform(compressor); - - var endTime = Date.now(); - debug('Uglify step 3 took %dms', endTime - startTime); - deferred.resolve(compressed_ast); - return deferred.promise; - } - - function splittedUglifyStep4(compressed_ast) { - var deferred = Q.defer(); - var startTime = Date.now(); - - compressed_ast.figure_out_scope(); - - var endTime = Date.now(); - debug('Uglify step 4 took %dms', endTime - startTime); - deferred.resolve(compressed_ast); - return deferred.promise; - } - - function splittedUglifyStep5(compressed_ast) { - var deferred = Q.defer(); - var startTime = Date.now(); - - compressed_ast.compute_char_frequency(); - - var endTime = Date.now(); - debug('Uglify step 5 took %dms', endTime - startTime); - deferred.resolve(compressed_ast); - return deferred.promise; - } - - function splittedUglifyStep6(compressed_ast) { - var deferred = Q.defer(); - var startTime = Date.now(); - - compressed_ast.mangle_names(); - - var endTime = Date.now(); - debug('Uglify step 6 took %dms', endTime - startTime); - deferred.resolve(compressed_ast); - return deferred.promise; - } - - function splittedUglifyStep7(compressed_ast) { - var deferred = Q.defer(); - var startTime = Date.now(); - - var code = compressed_ast.print_to_string(); - - var endTime = Date.now(); - debug('Uglify step 7 took %dms', endTime - startTime); - deferred.resolve(code); return deferred.promise; } diff --git a/package.json b/package.json index 4faaed0..d168c9d 100644 --- a/package.json +++ b/package.json @@ -16,72 +16,71 @@ "yellowlabtools": "./bin/cli.js" }, "engines": { - "node": ">= 4.0" + "node": ">= 6.0" }, "main": "./lib/index.js", "dependencies": { - "angular": "1.6.8", - "angular-animate": "1.6.8", + "angular": "1.7.7", + "angular-animate": "1.7.7", "angular-chart.js": "1.1.1", "angular-local-storage": "0.7.1", - "angular-resource": "1.6.8", - "angular-route": "1.6.8", - "angular-sanitize": "1.6.8", - "async": "2.6.0", - "body-parser": "1.18.2", - "chart.js": "2.7.1", - "clean-css": "4.1.9", + "angular-resource": "1.7.7", + "angular-route": "1.7.7", + "angular-sanitize": "1.7.7", + "async": "2.6.1", + "body-parser": "1.18.3", + "chart.js": "2.7.3", + "clean-css": "4.2.1", "color-diff": "1.1.0", - "compression": "1.7.1", - "cors": "2.8.4", + "compression": "1.7.3", + "cors": "2.8.5", "css-mq-parser": "0.0.3", - "debug": "3.1.0", + "debug": "4.1.1", "easyxml": "2.0.1", - "ejs": "2.5.7", - "express": "4.16.2", - "fontkit": "1.7.7", + "ejs": "2.6.1", + "express": "4.16.4", + "fontkit": "1.7.8", "image-size": "0.7.1", - "imagemin": "5.3.1", - "imagemin-jpegoptim": "5.2.0", - "imagemin-jpegtran": "5.0.2", - "imagemin-optipng": "5.2.1", - "imagemin-svgo": "6.0.0", + "imagemin": "6.1.0", + "imagemin-jpegoptim": "6.0.0", + "imagemin-jpegtran": "6.0.0", + "imagemin-optipng": "6.0.0", + "imagemin-svgo": "7.0.0", "is-eot": "1.0.0", - "is-gif": "1.0.0", + "is-gif": "3.0.0", "is-http2": "1.2.0", - "is-jpg": "1.0.0", + "is-jpg": "2.0.0", "is-otf": "0.1.2", "is-png": "1.1.0", - "is-svg": "2.1.0", + "is-svg": "3.0.0", "is-ttf": "0.2.2", "is-woff": "1.0.3", "is-woff2": "1.0.0", - "jimp": "0.2.28", + "jimp": "0.6.0", "md5": "2.2.1", - "meow": "3.7.0", - "minimize": "2.1.0", + "meow": "5.0.0", + "minimize": "2.2.0", "parse-color": "1.0.0", "phantomas": "1.19.0", "ps-node": "0.1.6", "q": "1.5.1", - "request": "2.83.0", - "rimraf": "2.6.2", + "request": "2.88.0", + "rimraf": "2.6.3", "temporary": "0.0.8", - "try-thread-sleep": "1.0.2", - "uglify-js": "2.8.29" + "uglify-js": "3.4.9" }, "devDependencies": { - "chai": "~4.1.2", - "grunt": "~1.0.1", - "grunt-contrib-clean": "~1.1.0", + "chai": "~4.2.0", + "grunt": "~1.0.3", + "grunt-contrib-clean": "~2.0.0", "grunt-contrib-concat": "~1.0.1", "grunt-contrib-copy": "~1.0.0", - "grunt-contrib-cssmin": "~2.2.1", - "grunt-contrib-htmlmin": "~2.4.0", - "grunt-contrib-jshint": "~1.1.0", - "grunt-contrib-less": "~1.4.1", - "grunt-contrib-uglify": "~3.3.0", - "grunt-contrib-watch": "~1.0.0", + "grunt-contrib-cssmin": "~3.0.0", + "grunt-contrib-htmlmin": "~3.0.0", + "grunt-contrib-jshint": "~2.0.0", + "grunt-contrib-less": "~2.0.0", + "grunt-contrib-uglify": "~4.0.0", + "grunt-contrib-watch": "~1.1.0", "grunt-env": "~0.4.4", "grunt-express": "~1.4.1", "grunt-filerev": "~2.3.1", @@ -90,11 +89,11 @@ "grunt-parallel": "~0.5.1", "grunt-replace": "~1.0.1", "grunt-usemin": "~3.1.1", - "grunt-webfont": "~1.6.0", + "grunt-webfont": "~1.7.2", "matchdep": "~2.0.0", - "mocha": "~4.1.0", - "sinon": "~4.1.6", - "sinon-chai": "~2.14.0" + "mocha": "~5.2.0", + "sinon": "~7.2.3", + "sinon-chai": "~3.3.0" }, "scripts": { "test": "grunt test" diff --git a/server_config/server_install.sh b/server_config/server_install.sh index 062eae2..3c4b4f4 100644 --- a/server_config/server_install.sh +++ b/server_config/server_install.sh @@ -6,7 +6,7 @@ sudo apt-get install lsb-release libfontconfig1 libfreetype6 libjpeg-dev -y --fo sudo apt-get install curl git software-properties-common build-essential make g++ -y --force-yes > /dev/null 2>&1 # Installation of NodeJS -curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash - +curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - sudo apt-get install -y nodejs > /dev/null 2>&1 source ~/.profile diff --git a/test/core/phantomasWrapperTest.js b/test/core/phantomasWrapperTest.js index b6d6372..1be8a88 100644 --- a/test/core/phantomasWrapperTest.js +++ b/test/core/phantomasWrapperTest.js @@ -61,36 +61,4 @@ describe('phantomasWrapper', function() { } }); }); - - it('should timeout but return some results', function(done) { - var url = 'http://localhost:8388/simple-page.html'; - - this.timeout(5000); - phantomasWrapper.execute({ - params: { - url: url, - options: { - timeout: 1 - } - } - }).then(function(data) { - /*jshint -W030 */ - - try { - data.should.be.an('object'); - data.should.have.a.property('generator'); - data.generator.should.contain('phantomas'); - data.should.have.a.property('url').that.equals(url); - data.should.have.a.property('metrics').that.is.an('object').not.empty; - data.should.have.a.property('offenders').that.is.an('object').not.empty; - data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty; - - done(); - } catch(error) { - done(error); - } - }).fail(function(err) { - done(err); - }); - }); }); \ No newline at end of file From 5e82eea9f1595d0c373e8d05bcb5f560a92b33e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Wed, 13 Mar 2019 19:27:57 +0100 Subject: [PATCH 02/59] Remove JS Timeline and Scroll Bottlenecks --- front/src/css/timeline.css | 338 --------- front/src/js/app.js | 5 - front/src/js/controllers/timelineCtrl.js | 190 ------ .../src/js/directives/offendersDirectives.js | 644 ------------------ front/src/js/services/menuService.js | 3 - front/src/less/timeline.less | 387 ----------- front/src/main.html | 2 - front/src/views/resultSubHeader.html | 1 - front/src/views/rule.html | 33 - front/src/views/timeline.html | 100 --- lib/metadata/policies.js | 48 -- lib/metadata/scoreProfileGeneric.json | 17 - lib/runner.js | 4 - lib/server/controllers/frontController.js | 2 +- lib/tools/jsExecutionTransformer.js | 210 ------ .../custom_modules/modules/domQYLT/domQYLT.js | 387 ----------- .../custom_modules/modules/jQYLT/jQYLT.js | 381 ----------- .../javaScriptBottleYLT.js | 69 -- .../modules/jsTreeYLT/jsTreeYLT.js | 31 - .../modules/scrollListener/scrollListener.js | 69 -- .../modules/windowPerfYLT/windowPerfYLT.js | 150 ---- 21 files changed, 1 insertion(+), 3070 deletions(-) delete mode 100644 front/src/css/timeline.css delete mode 100644 front/src/js/controllers/timelineCtrl.js delete mode 100644 front/src/less/timeline.less delete mode 100644 front/src/views/timeline.html delete mode 100644 lib/tools/jsExecutionTransformer.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/domQYLT/domQYLT.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/javaScriptBottleYLT/javaScriptBottleYLT.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/scrollListener/scrollListener.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/windowPerfYLT/windowPerfYLT.js diff --git a/front/src/css/timeline.css b/front/src/css/timeline.css deleted file mode 100644 index 11920fb..0000000 --- a/front/src/css/timeline.css +++ /dev/null @@ -1,338 +0,0 @@ -/* Timeline colors, related to Window Performances */ -.execution { - text-align: center; -} -.selectScript { - padding-bottom: 2em; - font-size: 0.9em; -} -.selectScript select { - max-width: 30em; -} -.selectScript.empty { - font-size: 0.8em; -} -.selectScript.empty select { - width: 10em; -} -.timeline { - margin: 2em 0 5em; -} -.timeline .chart { - position: relative; - width: 100%; - border-bottom: 1px solid #000; -} -.timeline .startTime, -.timeline .endTime { - position: absolute; - bottom: 0.5em; - font-size: 0.8em; -} -.timeline .startTime { - left: 0em; -} -.timeline .endTime { - right: 0em; -} -.timeline .chartPoints { - display: table; - height: 100px; - width: 99%; - margin: 0 auto; -} -.timeline .interval { - display: table-cell; - position: relative; - height: 100px; - width: 0.5%; -} -.timeline .interval .color { - position: absolute; - bottom: 0; - width: 100%; -} -.timeline .interval .color.clickable { - cursor: pointer; -} -.timeline div.interval:hover { - background: #9C4274; -} -.timeline .interval:hover .color { - background: #F04DA7; -} -.timeline .domComplete.interval { - background: #EDE3FF; -} -.timeline .domComplete .color { - background: #C2A3FF; -} -.timeline .domContentLoadedEnd.interval { - background: #D8F0F0; -} -.timeline .domContentLoadedEnd .color { - background: #7ECCCC; -} -.timeline .domContentLoaded.interval { - background: #E0FFD1; -} -.timeline .domContentLoaded .color { - background: #A7E846; -} -.timeline .domInteractive.interval { - background: #FFFCCC; -} -.timeline .domInteractive .color { - background: #FFE433; -} -.timeline .domCreation.interval { - background: #FFE0CC; -} -.timeline .domCreation .color { - background: #FF6600; -} -.timeline .tooltip.detailsOverlay { - position: absolute; - display: none; - width: auto; - padding: 0.5em 1em; - top: -1.5em; - right: 1em; -} -.timeline .interval:hover .tooltip { - display: block; -} -.timeline .legend { - display: table; - width: 100%; - margin-top: 1em; -} -.timeline .legend > div { - display: table-row; -} -.timeline .legend > div > div { - position: relative; - display: table-cell; - margin-top: 1em; -} -.timeline .titles { - font-weight: bold; -} -.timeline .titles > div { - padding: 0 1em 0 2em; -} -.timeline .tips { - font-size: 0.7em; -} -.timeline .tips > div { - padding: 1em 1em 0 0; -} -.timeline .legend .color { - display: block; - position: absolute; - left: 0; - height: 1.5em; - width: 1.5em; - border-radius: 0.2em; -} -.filters { - margin: 1em auto; - padding: 0.5em; - min-width: 30em; - width: 30%; - border: 1px dotted #aaa; - text-align: left; -} -.subFilters { - margin-left: 3em; - font-size: 0.9em; -} -.table { - display: table; - width: 100%; - border-spacing: 0.25em; -} -.table > div, -.table > a { - display: table-row; -} -.table > .headers > div { - font-weight: bold; - padding: 0.5em 1em; -} -.table > div > div, -.table > a > div { - padding: 0.1em 1em; - background: #f2f2f2; - display: table-cell; - text-align: left; -} -.table > div.jsError > .type, -.table > div.jsError > .value { - color: #e74c3c; - font-weight: bold; -} -.table > div.windowPerformance > div, -.table > div.windowPerformance > div.startTime { - background: #EBD8E2; -} -.table > div.showingDetails > div { - background: #f1c40f; -} -.table > div.highlight > div.startTime { - background-color: #C0F090; -} -.table > div.highlight-remove { - transition: 3s; -} -.table > div.highlight-remove > div.startTime { - transition: background-color 3s ease-in; -} -.table > div > .index { - color: #bbb; - word-break: normal; -} -.table > div > .type { - white-space: nowrap; -} -.table .children { - margin-top: 0.2em; - font-size: 0.8em; - line-height: 1.6em; -} -.table .child { - margin-left: 0.5em; -} -.table .child > .child { - margin-left: 1em; -} -.table .child:before { - content: "↳"; -} -.table .child .childArgs { - display: none; -} -.table .child span { - position: relative; -} -.table .child span:hover { - background: #EBD8E2; -} -.table .child span:hover div { - display: inline-block; -} -.table .child span:hover .childArgs { - display: block; - position: absolute; - padding: 0 1em 0 2em; - left: 100%; - top: 0; - background: #EBD8E2; - line-height: 1.3em; - height: 1.3em; - z-index: 2; -} -.table .showingDetails .child span:hover { - background: inherit; -} -.table .showingDetails .child span:hover .childArgs { - display: none; -} -.table > div > .value { - width: 70%; - word-break: break-all; -} -.table > div > .details { - position: relative; -} -.table .details .icon-question { - color: #f1c40f; - cursor: pointer; -} -.table .icon-warning { - display: inline-block; - width: 0.8em; -} -.detailsOverlay { - display: none; - position: absolute; - right: 3em; - top: -3em; - width: 45em; - min-height: 1em; - padding: 0 1em 1em; - background: #fff; - border: 2px solid #f1c40f; - border-radius: 0.5em; - z-index: 2; -} -@media screen and (max-width: 1024px) { - .detailsOverlay { - width: 25em; - } -} -.showDetails .detailsOverlay { - display: block; -} -.detailsOverlay .closeBtn { - position: absolute; - top: 0.5em; - right: 0.5em; - color: #f1c40f; - cursor: pointer; -} -.detailsOverlay .advice { - color: #e74c3c; - font-weight: bold; -} -.detailsOverlay .trace { - word-break: break-all; -} -.table > div > .duration, -.table > div > .startTime { - text-align: center; - white-space: nowrap; -} -.table > div > .startTime.domComplete { - background: #EDE3FF; -} -.table > div > .startTime.domContentLoadedEnd { - background: #D8F0F0; -} -.table > div > .startTime.domContentLoaded { - background: #E0FFD1; -} -.table > div > .startTime.domInteractive { - background: #FFFCCC; -} -.table > div > .startTime.domCreation { - background: #FFE0CC; -} -.execution .icon-warning { - color: #e74c3c; - cursor: pointer; -} -.queryWithoutResultsFilterOn > div { - display: none; -} -.queryWithoutResultsFilterOn > div.queryWithoutResults { - display: table-row; -} -.jQueryCallOnEmptyObjectFilterOn > div { - display: none; -} -.jQueryCallOnEmptyObjectFilterOn > div.jQueryCallOnEmptyObject { - display: table-row; -} -.eventNotDelegatedFilterOn > div { - display: none; -} -.eventNotDelegatedFilterOn > div.eventNotDelegated { - display: table-row; -} -.jsErrorFilterOn > div { - display: none; -} -.jsErrorFilterOn > div.jsError { - display: table-row; -} diff --git a/front/src/js/app.js b/front/src/js/app.js index ef3d2b2..cdaf143 100644 --- a/front/src/js/app.js +++ b/front/src/js/app.js @@ -7,7 +7,6 @@ var yltApp = angular.module('YellowLabTools', [ 'queueCtrl', 'ruleCtrl', 'screenshotCtrl', - 'timelineCtrl', 'runsFactory', 'resultsFactory', 'apiService', @@ -64,10 +63,6 @@ yltApp.config(['$routeProvider', '$locationProvider', templateUrl: 'views/dashboard.html', controller: 'DashboardCtrl' }). - when('/result/:runId/timeline', { - templateUrl: 'views/timeline.html', - controller: 'TimelineCtrl' - }). when('/result/:runId/screenshot', { templateUrl: 'views/screenshot.html', controller: 'ScreenshotCtrl' diff --git a/front/src/js/controllers/timelineCtrl.js b/front/src/js/controllers/timelineCtrl.js deleted file mode 100644 index e23fee1..0000000 --- a/front/src/js/controllers/timelineCtrl.js +++ /dev/null @@ -1,190 +0,0 @@ -var timelineCtrl = angular.module('timelineCtrl', []); - -timelineCtrl.controller('TimelineCtrl', ['$scope', '$rootScope', '$routeParams', '$location', '$timeout', 'Menu', 'Results', 'API', function($scope, $rootScope, $routeParams, $location, $timeout, Menu, Results, API) { - $scope.runId = $routeParams.runId; - $scope.Menu = Menu.setCurrentPage('timeline', $scope.runId); - - function loadResults() { - // Load result if needed - if (!$rootScope.loadedResult || $rootScope.loadedResult.runId !== $routeParams.runId) { - Results.get({runId: $routeParams.runId, exclude: 'toolsResults'}, function(result) { - $rootScope.loadedResult = result; - $scope.result = result; - render(); - }); - } else { - $scope.result = $rootScope.loadedResult; - render(); - } - } - - function render() { - initFilters(); - initScriptFiltering(); - initExecutionTree(); - initTimeline(); - $timeout(initProfiler, 100); - } - - function initFilters() { - var hash = $location.hash(); - var filter = null; - - if (hash.indexOf('filter=') === 0) { - filter = hash.substr(7); - } - - $scope.warningsFilterOn = (filter !== null); - $scope.warningsFilters = { - queryWithoutResults: (filter === null || filter === 'queryWithoutResults'), - jQueryCallOnEmptyObject: (filter === null || filter === 'jQueryCallOnEmptyObject'), - eventNotDelegated: (filter === null || filter === 'eventNotDelegated'), - jsError: (filter === null || filter === 'jsError') - }; - } - - function initScriptFiltering() { - var offenders = $scope.result.rules.totalRequests.offendersObj.list.byType.js; - $scope.scripts = []; - - offenders.forEach(function(filePath) { - var shortPath = filePath; - - if (filePath.length > 100) { - shortPath = filePath.substr(0, 98) + '...'; - } - - var scriptObj = { - fullPath: filePath, - shortPath: shortPath - }; - - $scope.scripts.push(scriptObj); - }); - } - - function initExecutionTree() { - var originalExecutions = $scope.result.javascriptExecutionTree.children || []; - - // Detect the last event of all (before filtering) and read time - var lastEvent = originalExecutions[originalExecutions.length - 1]; - $scope.endTime = lastEvent.data.timestamp + (lastEvent.data.time || 0); - - // Filter - $scope.executionTree = []; - originalExecutions.forEach(function(node) { - - // Filter by script (if enabled) - if ($scope.selectedScript) { - if (node.data.backtrace && node.data.backtrace.indexOf($scope.selectedScript.fullPath + ':') === -1) { - return; - } - if (node.data.type === "jQuery loaded" || node.data.type === "jQuery version change") { - return; - } - } - - $scope.executionTree.push(node); - }); - } - - function initTimeline() { - - // Split the timeline into 200 intervals - var numberOfIntervals = 199; - $scope.timelineIntervalDuration = $scope.endTime / numberOfIntervals; - - // Pre-fill array of as many elements as there are milleseconds - var millisecondsArray = Array.apply(null, new Array($scope.endTime + 1)).map(Number.prototype.valueOf,0); - - // Create the milliseconds array from the execution tree - $scope.executionTree.forEach(function(node) { - if (node.data.time !== undefined) { - - // Ignore artefacts (durations > 100ms) - var time = Math.min(node.data.time, 100) || 1; - - for (var i=node.data.timestamp, max=node.data.timestamp + time ; i 0) { - break; - } - } - - return lineIndex; - }; - - - $scope.backToDashboard = function() { - $location.path('/result/' + $scope.runId); - }; - - $scope.testAgain = function() { - API.relaunchTest($scope.result); - }; - - loadResults(); - -}]); - -timelineCtrl.directive('scrollOnClick', ['$animate', '$timeout', function($animate, $timeout) { - return { - restrict: 'A', - link: function (scope, element, attributes) { - // When the user clicks on the timeline, find the right profiler line and scroll to it - element.on('click', function() { - var lineIndex = scope.findLineIndexByTimestamp(attributes.scrollOnClick); - var lineElement = angular.element(document.getElementById('line_' + lineIndex)); - - // Animate the background color to "flash" the row - lineElement.addClass('highlight'); - $timeout(function() { - $animate.removeClass(lineElement, 'highlight'); - scope.$digest(); - }, 50); - - - window.scrollTo(0, lineElement[0].offsetTop); - }); - } - }; -}]); diff --git a/front/src/js/directives/offendersDirectives.js b/front/src/js/directives/offendersDirectives.js index 434598a..9375973 100644 --- a/front/src/js/directives/offendersDirectives.js +++ b/front/src/js/directives/offendersDirectives.js @@ -93,511 +93,6 @@ }; }); - - function getJQueryContextButtonHTML(context, onASingleLine) { - if (context.length === 0) { - return 'Empty jQuery object'; - } - - if (context.length === 1) { - return getDomElementButtonHTML(context.elements[0], onASingleLine); - } - - var html = context.length + ' elements (' + getDomElementButtonHTML(context.elements[0], onASingleLine) + ', ' + getDomElementButtonHTML(context.elements[1], onASingleLine); - if (context.length === 3) { - html += ', ' + getDomElementButtonHTML(context.elements[0], onASingleLine); - } else if (context.length > 3) { - html += ' and ' + (context.length - 2) + ' more...'; - } - return html + ')'; - } - - function isJQuery(node) { - return node.data.type.indexOf('jQuery ') === 0; - } - - function getNonJQueryHTML(node, onASingleLine) { - var type = node.data.type; - - if (node.windowPerformance) { - switch (type) { - case 'documentScroll': - return '(triggering the scroll event on document)'; - - case 'windowScroll': - return '(triggering the scroll event on window)'; - - case 'window.onscroll': - return '(calling the window.onscroll function)'; - - default: - return ''; - } - } - - if (!node.data.callDetails) { - return ''; - } - - var args = node.data.callDetails.arguments; - var ctxt = node.data.callDetails.context; - - - switch (type) { - case 'getElementById': - case 'createElement': - return '' + args[0] + ''; - - case 'getElementsByClassName': - case 'getElementsByTagName': - case 'querySelector': - case 'querySelectorAll': - return '' + args[0] + ' on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine); - - case 'appendChild': - return 'append ' + getDomElementButtonHTML(args[0], onASingleLine) + ' to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine); - - case 'insertBefore': - return 'insert ' + getDomElementButtonHTML(args[0], onASingleLine) + ' into ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine) + ' before ' + getDomElementButtonHTML(args[1], onASingleLine); - - case 'addEventListener': - return 'bind ' + args[0] + ' to ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine); - - case 'getComputedStyle': - return getDomElementButtonHTML(args[0], onASingleLine) + (args[1] || ''); - - case 'error': - return args[0]; - - case 'jQuery - onDOMReady': - return '(function)'; - - case 'documentScroll': - return 'The scroll event just triggered on document'; - - case 'windowScroll': - return 'The scroll event just triggered on window'; - - case 'window.onscroll': - return 'The window.onscroll function just got called'; - - default: - return ''; - } - } - - function getJQueryHTML(node, onASingleLine) { - var type = node.data.type; - var unescapedArgs = node.data.callDetails.arguments; - var args = []; - var ctxt = node.data.callDetails.context; - - // escape HTML in args - for (var i = 0 ; i < 4 ; i ++) { - if (unescapedArgs[i] !== undefined) { - args[i] = escapeHTML(unescapedArgs[i]); - } - } - - if (type === 'jQuery loaded' || type === 'jQuery version change') { - return args[0]; - } - - switch (type) { - case 'jQuery - onDOMReady': - case 'jQuery - windowOnLoad': - return '(function)'; - - case 'jQuery - Sizzle call': - return '' + args[0] + ' on ' + getDomElementButtonHTML(ctxt.elements[0], onASingleLine); - - case 'jQuery - find': - if (ctxt && ctxt.length === 1 && ctxt.elements[0].type !== 'document') { - return '' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } else { - return '' + args[0] + ''; - } - break; - - case 'jQuery - html': - if (args[0] !== undefined) { - return 'set content "' + args[0] + '" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } else { - return 'get content from ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - append': - return 'append ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - - case 'jQuery - appendTo': - return 'append ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to ' + args[0] + ''; - - case 'jQuery - prepend': - return 'prepend ' + joinArgs(args) + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - - case 'jQuery - prependTo': - return 'prepend ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to ' + args[0] + ''; - - case 'jQuery - before': - return 'insert ' + joinArgs(args) + ' before ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - - case 'jQuery - insertBefore': - return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' before ' + args[0] + ''; - - case 'jQuery - after': - return 'insert ' + joinArgs(args) + ' after ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - - case 'jQuery - insertAfter': - return 'insert ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' after ' + args[0] + ''; - - case 'jQuery - remove': - case 'jQuery - detach': - if (args[0]) { - return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by ' + args[0] + ''; - } else { - return getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - empty': - case 'jQuery - clone': - case 'jQuery - unwrap': - case 'jQuery - show': - case 'jQuery - hide': - case 'jQuery - animate': - case 'jQuery - fadeIn': - case 'jQuery - fadeOut': - case 'jQuery - fadeTo': - case 'jQuery - fadeToggle': - case 'jQuery - slideDown': - case 'jQuery - slideUp': - case 'jQuery - slideToggle': - return getJQueryContextButtonHTML(ctxt, onASingleLine); - - case 'jQuery - replaceWith': - return 'replace ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' with ' + args[0] + ''; - - case 'jQuery - replaceAll': - return 'replace ' + args[0] + ' with ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - - case 'jQuery - text': - if (args[0] !== undefined) { - return 'set text "' + args[0] + '" to ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } else { - return 'get text from ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - wrap': - case 'jQuery - wrapAll': - return 'wrap ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within ' + args[0] + ''; - - case 'jQuery - wrapInner': - return 'wrap the content of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' within ' + args[0] + ''; - - case 'jQuery - css': - case 'jQuery - attr': - case 'jQuery - prop': - if (isStringOfObject(args[0])) { - return 'set ' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } else if (args[1]) { - return 'set ' + args[0] + ' : ' + args[1] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } else { - return 'get ' + args[0] + ' from ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - offset': - case 'jQuery - height': - case 'jQuery - innerHeight': - case 'jQuery - width': - case 'jQuery - innerWidth': - case 'jQuery - scrollLeft': - case 'jQuery - scrollTop': - case 'jQuery - position': - if (args[0]) { - return 'set ' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } else { - return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - outerHeight': - case 'jQuery - outerWidth': - if (args[0] && args[0] !== 'true') { - return 'set ' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } else if (args[0] === 'true') { - return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' (with include margins option)'; - } else { - return 'get from ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - toggle': - if (args[0] === 'true') { - return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to visible'; - } else if (args[0] === 'false') { - return getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to hidden'; - } else { - return getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - on': - case 'jQuery - one': - if (isStringOfObject(args[0])) { - return '' + args[0].replace(/"\(function\)"/g, '(function)') + ''; - } else if (args[1] && isPureString(args[1])) { - return 'bind ' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by ' + args[1] + ''; - } else { - return 'bind ' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - off': - if (args[0]) { - if (args[1]) { - return 'unbind ' + args[0] + ' from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by ' + args[1] + ''; - } else { - return 'unbind ' + args[0] + ' from ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - } else { - return 'unbind all events'; - } - break; - - case 'jQuery - live': - case 'jQuery - bind': - return 'bind ' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - - case 'jQuery - die': - case 'jQuery - unbind': - if (args[0]) { - return 'unbind ' + args[0] + ' from ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } else { - return 'unbind all events'; - } - break; - - case 'jQuery - delegate': - return 'bind ' + args[1] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by ' + args[0] + ''; - - case 'jQuery - undelegate': - if (args[0]) { - if (args[1]) { - return 'unbind ' + args[1] + ' from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + '\'s children filtered by ' + args[0] + ''; - } else { - return 'unbind namespace ' + args[0] + ''; - } - } else { - return 'unbind all events'; - } - break; - - case 'jQuery - blur': - case 'jQuery - change': - case 'jQuery - click': - case 'jQuery - dblclick': - case 'jQuery - focus': - case 'jQuery - keydown': - case 'jQuery - keypress': - case 'jQuery - keyup': - case 'jQuery - mousedown': - case 'jQuery - mouseenter': - case 'jQuery - mouseleave': - case 'jQuery - mousemove': - case 'jQuery - mouseout': - case 'jQuery - mouseover': - case 'jQuery - mouseup': - case 'jQuery - resize': - case 'jQuery - scroll': - case 'jQuery - select': - case 'jQuery - submit': - if (args[0]) { - return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } else { - return 'triggered on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - error': - case 'jQuery - focusin': - case 'jQuery - focusout': - case 'jQuery - hover': - case 'jQuery - load': - case 'jQuery - unload': - return 'bind on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - - case 'jQuery - removeAttr': - case 'jQuery - removeProp': - return 'remove ' + args[0] + ' from ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - - case 'jQuery - val': - if (args[0]) { - return 'set value ' + args[0] + ' to ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } else { - return 'get value from ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - hasClass': - case 'jQuery - addClass': - case 'jQuery - removeClass': - return '' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - - case 'jQuery - toggleClass': - if (args[0]) { - if (args[1]) { - return 'toggle ' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' to ' + args[1] + ''; - } else { - return 'toggle ' + args[0] + ' on ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - } else { - return 'magic no-argument toggleClass'; - } - break; - - case 'jQuery - children': - if (args[0]) { - return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' filtered by ' + args[0] + ''; - } else { - return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - closest': - if (args[1]) { - return 'closest ' + args[0] + ' from ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' in context ' + args[1] + ''; - } else { - return 'closest ' + args[0] + ' from ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - next': - case 'jQuery - nextAll': - if (args[0]) { - return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching ' + args[0] + ''; - } else { - return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - nextUntil': - if (args[0]) { - if (args[1]) { - return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until ' + args[0] + ' and matching ' + args[1] + ''; - } else { - return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until ' + args[0] + ''; - } - } else { - return 'after ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - offsetParent': - return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - - case 'jQuery - prev': - case 'jQuery - prevAll': - if (args[0]) { - return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching ' + args[0] + ''; - } else { - return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - prevUntil': - if (args[0]) { - if (args[1]) { - return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until ' + args[0] + ' and matching ' + args[1] + ''; - } else { - return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until ' + args[0] + ''; - } - } else { - return 'before ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - parent': - case 'jQuery - parents': - if (args[0]) { - return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching ' + args[0] + ''; - } else { - return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - parentsUntil': - if (args[0]) { - if (args[1]) { - return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until ' + args[0] + ' and matching ' + args[1] + ''; - } else { - return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' until ' + args[0] + ''; - } - } else { - return 'of ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - case 'jQuery - siblings': - if (args[0]) { - return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine) + ' matching ' + args[0] + ''; - } else { - return 'near ' + getJQueryContextButtonHTML(ctxt, onASingleLine); - } - break; - - default: - return ''; - } - } - - function escapeHTML(html) { - var entityMap = { - "&": "&", - "<": "<", - ">": ">", - '"': '"', - "'": ''', - "/": '/' - }; - - return String(html).replace(/[&<>"'\/]/g, function (s) { - return entityMap[s]; - }); - } - - function joinArgs(args) { - var html = '' + args[0] + ''; - if (args[1]) { - html += ', ' + args[1] + ''; - if (args[2]) { - html += ', ' + args[2] + ''; - if (args[3]) { - html += ', and more...'; - } - } - } - return html; - } - - function isStringOfObject(str) { - return typeof str === 'string' && str[0] === '{' && str[str.length - 1] === '}'; - } - - function isPureString(str) { - return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown'; - } - - function getTimelineParamsHTML(node, onASingleLine) { - if (isJQuery(node)) { - return getJQueryHTML(node, onASingleLine); - } else { - return getNonJQueryHTML(node, onASingleLine); - } - } - function getBacktraceHTML(backtrace) { var html = ''; var parsedBacktrace = parseBacktrace(backtrace); @@ -687,145 +182,6 @@ return out; } - function getTimelineDetailsHTML(node) { - var html = ''; - - if (node.data.type != 'jQuery loaded' && node.data.type != 'jQuery version change' && !node.windowPerformance) { - if (node.warning || node.error) { - html += '
'; - } else { - html += '
'; - } - - html += '
'; - html += '
'; - - if (node.data.callDetails.context && node.data.callDetails.context.length === 0) { - html += '

Called on 0 jQuery element

Useless function call, as the jQuery object is empty.

'; - } else if (node.eventNotDelegated) { - html += '

This binding should use Event Delegation instead of binding each element one by one.

'; - } - - if (node.data.resultsNumber === 0) { - html += '

The query returned 0 results. Could it be unused or dead code?

'; - } else if (node.data.resultsNumber > 0) { - html += '

The query returned ' + node.data.resultsNumber + ' ' + (node.data.resultsNumber > 1 ? 'results' : 'result') + '.

'; - } - - if (node.data.backtrace) { - html += '

Backtrace

'; - html += '
'; - html += getBacktraceHTML(node.data.backtrace); - html += '
'; - } - - html += '
'; - } - - return html; - } - - - offendersDirectives.directive('profilerLine', ['$filter', function($filter) { - - var numberWithCommas = $filter('number'); - - function getProfilerLineHTML(index, node) { - return '
' + (index + 1) + '
' + - '
' + node.data.type + (node.children ? '
' + recursiveChildrenHTML(node) + '
' : '') + '
' + - '
' + getTimelineParamsHTML(node, false) + '
' + - '
' + getTimelineDetailsHTML(node) + '
' + - '
' + numberWithCommas(node.data.timestamp, 0) + ' ms
'; - } - - function recursiveChildrenHTML(node) { - var html = ''; - - if (node.children) { - node.children.forEach(function(child) { - html += '
' + child.data.type + '
' + getTimelineParamsHTML(child, true) + '
' + recursiveChildrenHTML(child) + '
'; - }); - } - - return html; - } - - function onDetailsClick(row) { - // Close if it's already open - if (row.classList.contains('showDetails')) { - closeDetails(row); - return; - } - - // Close any other open details overlay - var openOnes = document.getElementsByClassName('showDetails'); - if (openOnes.length > 0) { - openOnes[0].classList.remove('showDetails'); - } - - // Make it appear - row.classList.add('showDetails'); - - // Bind the close button - row.querySelector('.closeBtn').addEventListener('click', function() { - closeDetails(row); - }); - } - - function closeDetails(row) { - row.classList.remove('showDetails'); - - // Unbind the close button - row.querySelector('.closeBtn').removeEventListener('click', closeDetails); - } - - return { - restrict: 'E', - scope: { - index: '=', - node: '=' - }, - template: '
', - replace: true, - link: function(scope, element) { - - if (scope.node.error) { - element.addClass('jsError'); - } else if (scope.node.windowPerformance) { - element.addClass('windowPerformance'); - } - - element.append(getProfilerLineHTML(scope.index, scope.node)); - element[0].id = 'line_' + scope.index; - - if (scope.node.warning) { - element[0].classList.add('warning'); - - if (scope.node.queryWithoutResults) { - element[0].classList.add('queryWithoutResults'); - } - - if (scope.node.jQueryCallOnEmptyObject) { - element[0].classList.add('jQueryCallOnEmptyObject'); - } - - if (scope.node.eventNotDelegated) { - element[0].classList.add('eventNotDelegated'); - } - } - - - // Bind click on the details icon - var detailsIcon = element[0].querySelector('.details div'); - if (detailsIcon) { - detailsIcon.addEventListener('click', function() { - onDetailsClick(this.parentNode.parentNode); - }); - } - } - }; - }]); - function shortenUrl(url, maxLength) { if (!maxLength) { maxLength = 110; diff --git a/front/src/js/services/menuService.js b/front/src/js/services/menuService.js index 7b56c74..9ef3c37 100644 --- a/front/src/js/services/menuService.js +++ b/front/src/js/services/menuService.js @@ -22,9 +22,6 @@ menuService.factory('Menu', ['$location', function($location) { case 'dashboard': $location.path('/result/' + currentRunId); break; - case 'timeline': - $location.path('/result/' + currentRunId + '/timeline'); - break; default: console.err('Undefined Menu.changePage() destination'); } diff --git a/front/src/less/timeline.less b/front/src/less/timeline.less deleted file mode 100644 index d59beac..0000000 --- a/front/src/less/timeline.less +++ /dev/null @@ -1,387 +0,0 @@ -/* Timeline colors, related to Window Performances */ -@domCreationColor: #FF6600; -@domCreationBg: #FFE0CC; -@domContentLoadedColor: #A7E846; -@domContentLoadedBg: #E0FFD1; -@domContentLoadedEndColor: #7ECCCC; -@domContentLoadedEndBg: #D8F0F0; -@domCompleteColor: #C2A3FF; -@domCompleteBg: #EDE3FF; -@domInteractiveColor: #FFE433; -@domInteractiveBg: #FFFCCC; - -.execution { - text-align: center; -} - -.selectScript { - padding-bottom: 2em; - font-size: 0.9em; - - select { - max-width: 30em; - } - - &.empty { - font-size: 0.8em; - - select { - width: 10em; - } - } -} - -.timeline { - margin: 2em 0 5em; -} -.timeline .chart { - position: relative; - width: 100%; - border-bottom: 1px solid #000; -} -.timeline .startTime, .timeline .endTime { - position: absolute; - bottom: 0.5em; - font-size: 0.8em; -} -.timeline .startTime { - left: 0em; -} -.timeline .endTime { - right: 0em; -} -.timeline .chartPoints { - display: table; - height: 100px; - width: 99%; - margin: 0 auto; -} -.timeline .interval { - display: table-cell; - position: relative; - height: 100px; - width: 0.5%; -} -.timeline .interval .color { - position: absolute; - bottom: 0; - width: 100%; - - &.clickable { - cursor: pointer; - } -} -.timeline div.interval:hover { - background: #9C4274; -} -.timeline .interval:hover .color { - background: #F04DA7; -} -.timeline .domComplete.interval { - background: @domCompleteBg; -} -.timeline .domComplete .color { - background: @domCompleteColor; -} -.timeline .domContentLoadedEnd.interval { - background: @domContentLoadedEndBg; -} -.timeline .domContentLoadedEnd .color { - background: @domContentLoadedEndColor; -} -.timeline .domContentLoaded.interval { - background: @domContentLoadedBg; -} -.timeline .domContentLoaded .color { - background: @domContentLoadedColor; -} -.timeline .domInteractive.interval { - background: @domInteractiveBg; -} -.timeline .domInteractive .color { - background: @domInteractiveColor; -} -.timeline .domCreation.interval { - background: @domCreationBg; -} -.timeline .domCreation .color { - background: @domCreationColor; -} -.timeline .tooltip.detailsOverlay { - position: absolute; - display: none; - width: auto; - padding: 0.5em 1em; - top: -1.5em; - right: 1em; -} -.timeline .interval:hover .tooltip { - display: block; -} - -.timeline .legend { - display: table; - width: 100%; - margin-top: 1em; -} -.timeline .legend > div { - display: table-row; -} -.timeline .legend > div > div { - position: relative; - display: table-cell; - margin-top: 1em; -} -.timeline .titles { - font-weight: bold; -} -.timeline .titles > div { - padding: 0 1em 0 2em; -} -.timeline .tips { - font-size: 0.7em; -} -.timeline .tips > div { - padding: 1em 1em 0 0; -} -.timeline .legend .color { - display: block; - position: absolute; - left: 0; - height: 1.5em; - width: 1.5em; - border-radius: 0.2em; -} - -.filters { - margin: 1em auto; - padding: 0.5em; - min-width: 30em; - width: 30%; - border: 1px dotted #aaa; - text-align: left; -} - -.subFilters { - margin-left: 3em; - font-size: 0.9em; -} - -.table { - display: table; - width: 100%; - border-spacing: 0.25em; -} - -.table > div, -.table > a { - display: table-row; -} - -.table > .headers > div { - font-weight: bold; - padding: 0.5em 1em; -} - -.table > div > div, -.table > a > div { - padding: 0.1em 1em; - background: #f2f2f2; - display: table-cell; - text-align: left; -} -.table > div.jsError > .type, .table > div.jsError > .value { - color: #e74c3c; - font-weight: bold; -} -.table > div.windowPerformance > div, .table > div.windowPerformance > div.startTime { - background: #EBD8E2; -} -.table > div.showingDetails > div { - background: #f1c40f; -} - -.table > div.highlight { - > div.startTime { - background-color: #C0F090; - } -} -.table > div.highlight-remove { - transition: 3s; - - > div.startTime { - transition: background-color 3s ease-in; - } -} - -.table > div > .index { - color: #bbb; - word-break: normal; -} - -.table > div > .type { - white-space:nowrap; -} - -.table .children { - margin-top: 0.2em; - font-size: 0.8em; - line-height: 1.6em; -} - -.table .child { - margin-left: 0.5em; - - > .child { - margin-left: 1em; - } - - &:before { - content: "↳"; - } - - .childArgs { - display: none; - } - - span { - position: relative; - } - - span:hover { - background: #EBD8E2; - - div { - display: inline-block; - } - - .childArgs { - display: block; - position: absolute; - padding: 0 1em 0 2em; - left: 100%; - top: 0; - background: #EBD8E2; - line-height: 1.3em; - height: 1.3em; - z-index: 2; - } - } -} - -.table .showingDetails .child span:hover { - background: inherit; - - .childArgs { - display: none; - } -} - -.table > div > .value { - width: 70%; - word-break: break-all; -} - -.table > div > .details { - position: relative; -} -.table .details .icon-question { - color: #f1c40f; - cursor: pointer; -} -.table .icon-warning { - display: inline-block; - width: 0.8em; -} - -.detailsOverlay { - display: none; - position: absolute; - right: 3em; - top: -3em; - width: 45em; - min-height: 1em; - padding: 0 1em 1em; - background: #fff; - border: 2px solid #f1c40f; - border-radius: 0.5em; - z-index: 2; -} -@media screen and (max-width: 1024px) { - .detailsOverlay { - width: 25em; - } -} -.showDetails .detailsOverlay { - display: block; -} -.detailsOverlay .closeBtn { - position: absolute; - top: 0.5em; - right: 0.5em; - color: #f1c40f; - cursor: pointer; -} -.detailsOverlay .advice { - color: #e74c3c; - font-weight: bold; -} -.detailsOverlay .trace { - word-break: break-all; -} - -.table > div > .duration, .table > div > .startTime { - text-align: center; - white-space:nowrap; -} -.table > div > .startTime.domComplete { - background: @domCompleteBg; -} -.table > div > .startTime.domContentLoadedEnd { - background: @domContentLoadedEndBg; -} -.table > div > .startTime.domContentLoaded { - background: @domContentLoadedBg; -} -.table > div > .startTime.domInteractive { - background: @domInteractiveBg; -} -.table > div > .startTime.domCreation { - background: @domCreationBg; -} -.execution .icon-warning { - color: #e74c3c; - cursor: pointer; -} -.queryWithoutResultsFilterOn { - > div { - display: none; - &.queryWithoutResults { - display: table-row; - } - } -} -.jQueryCallOnEmptyObjectFilterOn { - > div { - display: none; - &.jQueryCallOnEmptyObject { - display: table-row; - } - } -} -.eventNotDelegatedFilterOn { - > div { - display: none; - &.eventNotDelegated { - display: table-row; - } - } -} -.jsErrorFilterOn { - > div { - display: none; - &.jsError { - display: table-row; - } - } -} \ No newline at end of file diff --git a/front/src/main.html b/front/src/main.html index eec3f07..231289c 100644 --- a/front/src/main.html +++ b/front/src/main.html @@ -14,7 +14,6 @@ - @@ -51,7 +50,6 @@ - diff --git a/front/src/views/resultSubHeader.html b/front/src/views/resultSubHeader.html index 27de375..d31262a 100644 --- a/front/src/views/resultSubHeader.html +++ b/front/src/views/resultSubHeader.html @@ -4,5 +4,4 @@
New test
Test again
- \ No newline at end of file diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 2348306..105da75 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -229,19 +229,6 @@ -
-

The table below shows the interactions between the JavaScript and the DOM on a scroll event.

-
-
-
-
Type
-
Params
-
-
- -
-
-

This is the colors palette, sized by total occurrences:

@@ -378,26 +365,6 @@
-
-

{{rule.value}} offenders

- Please open the JS timeline -
- -
-

{{rule.value}} offenders

- Please open the JS timeline, filtered by "Queries without results" -
- -
-

{{rule.value}} offenders

- Please open the JS timeline, filtered by "jQuery calls on empty object" -
- -
-

{{rule.value}} offenders

- Please open the JS timeline, filtered by "Events not delegated" -
-

Breakpoints distribution graph

diff --git a/front/src/views/timeline.html b/front/src/views/timeline.html deleted file mode 100644 index b774c9a..0000000 --- a/front/src/views/timeline.html +++ /dev/null @@ -1,100 +0,0 @@ -
-
-
- Filter timeline and profiler by script: - -
- -

JavaScript Timeline

-

This graph gives a quick view of when the JavaScript interactions with the DOM occur during the loading of the page.

- -
-
-
-
-
-
-
- Timestamp: {{$index * timelineIntervalDuration | number: 0}} ms -
-
-
-
-
0 ms
-
{{endTime | number: 0}} ms
-
-
-
-
DOM creation
-
DOM interactive
-
DOM content loaded event
-
Page completion
-
Page is complete
-
-
-
Executing JavaScript and DOM queries here is a bad practice and slows down the DOM construction.
-
Some frameworks do things here, but it's not reliable and should be avoided.
-
Also known as "document ready". This is where you should execute top-priority scripts, like binding action buttons or launch a video player.
-
Here you can execute mid-priority tasks. Loading a script with createElement('script') is one way to do so.
-
The page is considered loaded, it's time for low priority things : trackers, social plugins, easter egg...
-
-
-
- -

JavaScript Profiler

-

- The table below shows the interactions between the JavaScript and the DOM. It is useful to understand what happens while the page loads. -

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- - -
-
-
- {{queryWithoutResultsFilterOn}} -
-
-
-
-
Type
-
Params
-
-
Timestamp
-
- - - -
- - -
\ No newline at end of file diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 7346224..17ca8ac 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -66,24 +66,6 @@ var policies = { }; } }, - "DOMaccesses": { - "tool": "jsExecutionTransformer", - "label": "DOM access", - "message": "

This metric counts the number of calls to DOM related functions (both native DOM functions and jQuery functions) on page load.

The more your JavaScript code accesses the DOM, the slower the page will load.

Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.

Try to reduce the number of queries by refactoring your JavaScript code.

Binding too many events also has a cost. Try to use event delegation as much as possible.

", - "isOkThreshold": 200, - "isBadThreshold": 2000, - "isAbnormalThreshold": 4000, - "hasOffenders": false - }, - "queriesWithoutResults": { - "tool": "jsExecutionTransformer", - "label": "Queries without result", - "message": "

Number of queries that return no result. Both native and jQuery DOM requests are counted.

It suggests the query is not used on the page, probably because it is some dead code.

Or maybe the code is trying to find an HTML block that is not always here. Look at the JS Timeline to see if the scripts correctly figures out the HTML block is not here and immediatly stops interacting further with the DOM.

", - "isOkThreshold": 0, - "isBadThreshold": 150, - "isAbnormalThreshold": 250, - "hasOffenders": false - }, "DOMqueriesAvoidable": { "tool": "phantomas", "label": "Duplicated DOM queries", @@ -147,18 +129,6 @@ var policies = { }; } }, - "DOMaccessesOnScroll": { - "tool": "jsExecutionTransformer", - "label": "DOM access on scroll", - "message": "

This rule counts the number of DOM-accessing functions calls on a scroll event, such as queries, readings, writings, bindings and jQuery functions.

Two scroll events are triggered quickly, one after the other, and only the second one is analyzed so throttled functions are ignored.

One of the main reasons of a poor scrolling experience is when too much JS is executed on each scroll event. Note that some devices such as smartphones and MacBooks send more scroll events than others.

Reduce the number of DOM accesses inside scroll listeners. Put DOM queries outside them when possible. Use throttling or debouncing.

", - "isOkThreshold": 1, - "isBadThreshold": 20, - "isAbnormalThreshold": 35, - "hasOffenders": true, - "offendersTransformFn": function(offenders) { - return offenders; - } - }, "jsErrors": { "tool": "phantomas", "label": "JavaScript errors", @@ -348,24 +318,6 @@ var policies = { "isAbnormalThreshold": 2, "hasOffenders": true }, - "jQueryCallsOnEmptyObject": { - "tool": "jsExecutionTransformer", - "label": "Calls on empty objects", - "message": "

This metric counts the number of jQuery functions called on an empty jQuery object. The call was useless.

This can be helpful to detect dead or unused code.

", - "isOkThreshold": 1, - "isBadThreshold": 100, - "isAbnormalThreshold": 180, - "hasOffenders": false - }, - "jQueryNotDelegatedEvents": { - "tool": "jsExecutionTransformer", - "label": "Events not delegated", - "message": "

This is the number of events that are bound with the .bind() or the .on() function without using event delegation.

This means jQuery binds each element contained in the object one by one. This is bad for performance.

", - "isOkThreshold": 1, - "isBadThreshold": 100, - "isAbnormalThreshold": 180, - "hasOffenders": false - }, "cssParsingErrors": { "tool": "phantomas", "label": "CSS syntax error", diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index eb453e8..b4f75e7 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -32,21 +32,6 @@ "DOMidDuplicated": 1 } }, - "domManipulations": { - "label": "DOM manipulations", - "policies": { - "DOMaccesses": 3, - "queriesWithoutResults": 1, - "DOMqueriesAvoidable": 1 - } - }, - "scroll": { - "label": "Scroll bottlenecks", - "policies": { - "eventsScrollBound": 1, - "DOMaccessesOnScroll": 4 - } - }, "badJavascript": { "label": "Bad JavaScript", "policies": { @@ -116,8 +101,6 @@ "pageWeight": 3, "requests": 3, "domComplexity": 2, - "domManipulations": 2, - "scroll": 1, "badJavascript": 2, "jQuery": 1, "cssSyntaxError": 1, diff --git a/lib/runner.js b/lib/runner.js index 3648a73..aae7275 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -2,7 +2,6 @@ var Q = require('q'); var debug = require('debug')('ylt:runner'); var phantomasWrapper = require('./tools/phantomas/phantomasWrapper'); -var jsExecutionTransformer = require('./tools/jsExecutionTransformer'); var colorDiff = require('./tools/colorDiff'); var mediaQueriesChecker = require('./tools/mediaQueriesChecker'); var isHttp2 = require('./tools/isHttp2'); @@ -28,9 +27,6 @@ var Runner = function(params) { .then(function(phantomasResults) { data.toolsResults.phantomas = phantomasResults; - // Treat the JS Execution Tree from offenders - data = jsExecutionTransformer.transform(data); - // Compare colors data = colorDiff.compareAllColors(data); diff --git a/lib/server/controllers/frontController.js b/lib/server/controllers/frontController.js index 83b66c3..1a652fb 100644 --- a/lib/server/controllers/frontController.js +++ b/lib/server/controllers/frontController.js @@ -10,7 +10,7 @@ var FrontController = function(app) { var assetsPath = (app.get('env') === 'development') ? '../../../front/src' : '../../../front/build'; // Routes templating - var routes = ['/', '/about', '/result/:runId', '/result/:runId/timeline', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId']; + var routes = ['/', '/about', '/result/:runId', '/result/:runId/screenshot', '/result/:runId/rule/:policy', '/queue/:runId']; routes.forEach(function(route) { app.get(route, function(req, res) { diff --git a/lib/tools/jsExecutionTransformer.js b/lib/tools/jsExecutionTransformer.js deleted file mode 100644 index a28cff6..0000000 --- a/lib/tools/jsExecutionTransformer.js +++ /dev/null @@ -1,210 +0,0 @@ -var debug = require('debug')('ylt:jsExecutionTransformer'); - -var offendersHelpers = require('../offendersHelpers'); -var Collection = require('./phantomas/custom_modules/util/collection'); - -var jsExecutionTransformer = function() { - - this.transform = function(data) { - var javascriptExecutionTree = {}; - var jQueryFunctionsCollection = new Collection(); - - var metrics = { - domInteractive: 0, - domContentLoaded: 0, - domContentLoadedEnd: 0, - domComplete: 0, - - DOMaccesses: 0, - DOMaccessesOnScroll: 0, - queriesWithoutResults: 0 - }; - - var offenders = {}; - - var hasjQuery = (data.toolsResults.phantomas.metrics.jQueryVersionsLoaded > 0); - if (hasjQuery) { - metrics.jQueryCalls = 0; - metrics.jQueryCallsOnEmptyObject = 0; - metrics.jQueryNotDelegatedEvents = 0; - } - - try { - - debug('Starting JS execution transformation'); - javascriptExecutionTree = JSON.parse(data.toolsResults.phantomas.offenders.javascriptExecutionTree[0]); - - if (javascriptExecutionTree.children) { - javascriptExecutionTree.children.forEach(function(node, index) { - - var contextLength = (node.data.callDetails && node.data.callDetails.context) ? node.data.callDetails.context.length : null; - - if (isABindWithoutEventDelegation(node, contextLength)) { - metrics.jQueryNotDelegatedEvents += contextLength; - node.warning = true; - node.eventNotDelegated = true; - } - - if (node.data.resultsNumber === 0) { - metrics.queriesWithoutResults ++; - node.queryWithoutResults = true; - node.warning = true; - } - - if (contextLength === 0) { - metrics.jQueryCallsOnEmptyObject ++; - node.jQueryCallOnEmptyObject = true; - node.warning = true; - } - - if (node.data.type.indexOf('jQuery - ') === 0) { - metrics.jQueryCalls ++; - jQueryFunctionsCollection.push(node.data.type); - } - - // Mark errors with an error flag - if (node.data.type === 'error' || node.data.type === 'jQuery version change') { - node.error = true; - } - - // Mark a performance flag - if (['domInteractive', 'domContentLoaded', 'domContentLoadedEnd', 'domComplete'].indexOf(node.data.type) >= 0) { - node.windowPerformance = true; - - // Adjust the navigation timings (cause their not very well synchronised) - switch(node.data.type) { - case 'domInteractive': - javascriptExecutionTree.data.domInteractive = node.data.timestamp; - break; - case 'domContentLoaded': - javascriptExecutionTree.data.domContentLoaded = node.data.timestamp; - break; - case 'domContentLoadedEnd': - javascriptExecutionTree.data.domContentLoadedEnd = node.data.timestamp; - break; - case 'domComplete': - javascriptExecutionTree.data.domComplete = node.data.timestamp; - break; - } - } - // Fix rare bug when domComplete was never triggered - if (index === javascriptExecutionTree.children.length - 1 && !javascriptExecutionTree.data.domComplete) { - javascriptExecutionTree.data.domComplete = node.data.timestamp + 1000; - } - - // Transform domPaths into objects - changeListOfDomPaths(node); - - // Count the number of DOM accesses, by counting the tree leafs - metrics.DOMaccesses += countTreeLeafs(node); - }); - - } - debug('JS execution transformation complete'); - - - if (data.toolsResults.phantomas.offenders.scrollExecutionTree) { - debug('Starting scroll execution transformation'); - offenders.DOMaccessesOnScroll = JSON.parse(data.toolsResults.phantomas.offenders.scrollExecutionTree[0]); - if (offenders.DOMaccessesOnScroll.children) { - offenders.DOMaccessesOnScroll.children.forEach(function(node) { - - // Mark a event flag - if (['documentScroll', 'windowScroll', 'window.onscroll'].indexOf(node.data.type) >= 0) { - node.windowPerformance = true; - } - - // Transform domPaths into objects - changeListOfDomPaths(node); - - // Count the number of DOM accesses, by counting the tree leafs - metrics.DOMaccessesOnScroll += countTreeLeafs(node); - }); - } - debug('Scroll execution transformation complete'); - } else { - debug('Could not parse scrollExecutionTree'); - } - - } catch(err) { - throw err; - } - - data.javascriptExecutionTree = javascriptExecutionTree; - - data.toolsResults.jsExecutionTransformer = { - metrics: metrics, - offenders: offenders - }; - - return data; - }; - - function treeRecursiveParser(node, fn) { - if (node.children) { - node.children.forEach(function(child) { - treeRecursiveParser(child, fn); - }); - } - fn(node); - } - - function changeListOfDomPaths(rootNode) { - treeRecursiveParser(rootNode, function(node) { - - if (node.data.callDetails && node.data.callDetails.context && node.data.callDetails.context.length > 0) { - node.data.callDetails.context.elements = node.data.callDetails.context.elements.map(offendersHelpers.domPathToDomElementObj, offendersHelpers); - } - - if (node.data.type === 'appendChild' || node.data.type === 'insertBefore' || node.data.type === 'getComputedStyle') { - node.data.callDetails.arguments[0] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[0]); - } - - if (node.data.type === 'insertBefore') { - node.data.callDetails.arguments[1] = offendersHelpers.domPathToDomElementObj(node.data.callDetails.arguments[1]); - } - }); - } - - // Returns the number of leafs (nodes without children) - function countTreeLeafs(rootNode) { - var count = 0; - - treeRecursiveParser(rootNode, function(node) { - if (!node.children && - !node.error && - !node.windowPerformance && - node.data.type !== 'jQuery loaded') { - count ++; - } - }); - - return count; - } - - function isPureString(str) { - return typeof str === 'string' && str[0] !== '{' && str !== '(function)' && str !== '[Object]' && str !== '[Array]' && str !== 'true' && str !== 'false' && str !== 'undefined' && str !== 'unknown' && str !== 'null'; - } - - function isABindWithoutEventDelegation(node, contextLength) { - // Count only on larger bindings - if (contextLength <= 3) { - return false; - } - - if (node.data.type === 'jQuery - on' && node.data.callDetails.arguments[1] && !isPureString(node.data.callDetails.arguments[1])) { - return true; - } - - if (node.data.type.indexOf('jQuery - ') === 0 && node.children && node.children.length === 1) { - var child = node.children[0]; - if (child.data.type === 'jQuery - on' && child.data.callDetails.arguments[1] && !isPureString(child.data.callDetails.arguments[1])) { - return true; - } - } - - return false; - } -}; - -module.exports = new jsExecutionTransformer(); \ No newline at end of file diff --git a/lib/tools/phantomas/custom_modules/modules/domQYLT/domQYLT.js b/lib/tools/phantomas/custom_modules/modules/domQYLT/domQYLT.js deleted file mode 100644 index 5e963da..0000000 --- a/lib/tools/phantomas/custom_modules/modules/domQYLT/domQYLT.js +++ /dev/null @@ -1,387 +0,0 @@ -/** - * Analyzes DOM queries done via native DOM methods - */ -/* global Element: true, Document: true, Node: true, window: true */ - -exports.version = '0.10.a'; - -exports.module = function(phantomas) { - 'use strict'; - - phantomas.setMetric('DOMqueries'); // @desc number of all DOM queries @offenders - phantomas.setMetric('DOMqueriesWithoutResults'); // @desc number of DOM queries that retutned nothing @offenders - phantomas.setMetric('DOMqueriesById'); // @desc number of document.getElementById calls - phantomas.setMetric('DOMqueriesByClassName'); // @desc number of document.getElementsByClassName calls - phantomas.setMetric('DOMqueriesByTagName'); // @desc number of document.getElementsByTagName calls - phantomas.setMetric('DOMqueriesByQuerySelectorAll'); // @desc number of document.querySelector(All) calls - phantomas.setMetric('DOMinserts'); // @desc number of DOM nodes inserts - phantomas.setMetric('DOMqueriesDuplicated'); // @desc number of DOM queries called more than once - phantomas.setMetric('DOMqueriesAvoidable'); // @desc number of repeated uses of a duplicated query - - // fake native DOM functions - phantomas.on('init', function() { - phantomas.evaluate(function() { - (function(phantomas) { - function querySpy(type, query, fnName, context, hasNoResults) { - phantomas.emit('domQuery', type, query, fnName, context, hasNoResults); // @desc DOM query has been made - } - - phantomas.spy(Document.prototype, 'getElementById', function(id) { - phantomas.incrMetric('DOMqueriesById'); - phantomas.addOffender('DOMqueriesById', '#%s (in %s)', id, '#document'); - - phantomas.enterContext({ - type: 'getElementById', - callDetails: { - arguments: ['#' + id] - }, - backtrace: phantomas.getBacktrace() - }); - - }, function(result, args) { - var id = args ? '#' + args[0] : undefined; - - querySpy('id', id, 'getElementById', '#document', (result === null)); - - var moreData = { - resultsNumber : (result === null) ? 0 : 1 - }; - phantomas.leaveContext(moreData); - }); - - // selectors by class name - function selectorClassNameSpyBefore(className) { - /*jshint validthis: true */ - - var context = phantomas.getDOMPath(this); - - phantomas.incrMetric('DOMqueriesByClassName'); - phantomas.addOffender('DOMqueriesByClassName', '.%s (in %s)', className, context); - - phantomas.enterContext({ - type: 'getElementsByClassName', - callDetails: { - context: { - length: 1, - elements: [context] - }, - arguments: ['.' + className] - }, - backtrace: phantomas.getBacktrace() - }); - } - - function selectorClassNameAfter(result, args) { - /*jshint validthis: true */ - - var className = args ? '.' + args[0] : undefined; - var context = phantomas.getDOMPath(this); - - querySpy('class', className, 'getElementsByClassName', context, (result.length === 0)); - - var moreData = { - resultsNumber : (result && result.length > 0) ? result.length : 0 - }; - phantomas.leaveContext(moreData); - } - - phantomas.spy(Document.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter); - phantomas.spy(Element.prototype, 'getElementsByClassName', selectorClassNameSpyBefore, selectorClassNameAfter); - - // selectors by tag name - function selectorTagNameSpyBefore(tagName) { - /*jshint validthis: true */ - - var context = phantomas.getDOMPath(this); - - phantomas.incrMetric('DOMqueriesByTagName'); - phantomas.addOffender('DOMqueriesByTagName', '%s (in %s)', tagName, context); - - phantomas.enterContext({ - type: 'getElementsByTagName', - callDetails: { - context: { - length: 1, - elements: [context] - }, - arguments: [tagName] - }, - backtrace: phantomas.getBacktrace() - }); - } - - function selectorTagNameSpyAfter(result, args) { - /*jshint validthis: true */ - - var tagName = args ? args[0].toLowerCase() : undefined; - var context = phantomas.getDOMPath(this); - - querySpy('tag name', tagName, 'getElementsByTagName', context, (result.length === 0)); - - var moreData = { - resultsNumber : (result && result.length > 0) ? result.length : 0 - }; - phantomas.leaveContext(moreData); - } - - phantomas.spy(Document.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter); - phantomas.spy(Element.prototype, 'getElementsByTagName', selectorTagNameSpyBefore, selectorTagNameSpyAfter); - - - // selector queries - function selectorQuerySpy(selector, context) { - phantomas.incrMetric('DOMqueriesByQuerySelectorAll'); - phantomas.addOffender('DOMqueriesByQuerySelectorAll', '%s (in %s)', selector, context); - } - - function selectorQuerySpyBefore(selector) { - /*jshint validthis: true */ - - var context = phantomas.getDOMPath(this); - selectorQuerySpy(selector, context); - - phantomas.enterContext({ - type: 'querySelector', - callDetails: { - context: { - length: 1, - elements: [context] - }, - arguments: [selector] - }, - backtrace: phantomas.getBacktrace() - }); - } - - function selectorQuerySpyAfter(result, args) { - /*jshint validthis: true */ - - var selector = args ? args[0] : undefined; - var context = phantomas.getDOMPath(this); - - querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0)); - - var moreData = { - resultsNumber : result ? 1 : 0 - }; - phantomas.leaveContext(moreData); - } - - function selectorAllQuerySpyBefore(selector) { - /*jshint validthis: true */ - - var context = phantomas.getDOMPath(this); - selectorQuerySpy(selector, context); - - phantomas.enterContext({ - type: 'querySelectorAll', - callDetails: { - context: { - length: 1, - elements: [context] - }, - arguments: [selector] - }, - backtrace: phantomas.getBacktrace() - }); - } - - function selectorAllQuerySpryAfter(result, args) { - /*jshint validthis: true */ - - var selector = args ? args[0] : undefined; - var context = phantomas.getDOMPath(this); - - querySpy('selector', selector, 'querySelectorAll', context, (!result || result.length === 0)); - - var moreData = { - resultsNumber : (result && result.length > 0) ? result.length : 0 - }; - phantomas.leaveContext(moreData); - } - - phantomas.spy(Document.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter); - phantomas.spy(Document.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter); - phantomas.spy(Element.prototype, 'querySelector', selectorQuerySpyBefore, selectorQuerySpyAfter); - phantomas.spy(Element.prototype, 'querySelectorAll', selectorAllQuerySpyBefore, selectorAllQuerySpryAfter); - - - // count DOM inserts - function appendChild(child, element, context, appended) { - /*jshint validthis: true */ - - // ignore appending to the node that's not yet added to DOM tree - if (!element.parentNode) { - return; - } - - // don't count elements added to fragments as a DOM inserts (issue #350) - // DocumentFragment > div[0] - if (context.indexOf('DocumentFragment') === 0) { - return; - } - - phantomas.incrMetric('DOMinserts'); - phantomas.addOffender('DOMinserts', '"%s" appended to "%s"', appended, context); - - //phantomas.log('DOM insert: node "%s" appended to "%s"', appended, context); - } - - function appendChildSpyBefore(child) { - /*jshint validthis: true */ - - var context = phantomas.getDOMPath(this); - var appended = phantomas.getDOMPath(child); - appendChild(child, this, context, appended); - - phantomas.enterContext({ - type: 'appendChild', - callDetails: { - context: { - length: 1, - elements: [context] - }, - arguments: [appended] - }, - backtrace: phantomas.getBacktrace() - }); - } - - function insertBeforeSpyBefore(child, refElement) { - /*jshint validthis: true */ - - var context = phantomas.getDOMPath(this); - var appended = phantomas.getDOMPath(child); - var referent = phantomas.getDOMPath(refElement); - appendChild(child, this, context, appended); - - phantomas.enterContext({ - type: 'insertBefore', - callDetails: { - context: { - length: 1, - elements: [context] - }, - arguments: [ - appended, - referent - ] - }, - backtrace: phantomas.getBacktrace() - }); - } - - phantomas.spy(Node.prototype, 'appendChild', appendChildSpyBefore, function(result) { - phantomas.leaveContext(); - }); - phantomas.spy(Node.prototype, 'insertBefore', insertBeforeSpyBefore, function(result) { - phantomas.leaveContext(); - }); - - - phantomas.spy(Document.prototype, 'createElement', function(tagName) { - - phantomas.enterContext({ - type: 'createElement', - callDetails: { - arguments: [tagName] - }, - backtrace: phantomas.getBacktrace() - }); - - }, function(result, args) { - phantomas.leaveContext(); - }); - - - phantomas.spy(Document.prototype, 'createTextNode', function(text) { - - phantomas.enterContext({ - type: 'createTextNode', - callDetails: { - arguments: [text] - }, - backtrace: phantomas.getBacktrace() - }); - - }, function(result, args) { - phantomas.leaveContext(); - }); - - - phantomas.spy(Document.prototype, 'createDocumentFragment', function() { - - phantomas.enterContext({ - type: 'createDocumentFragment', - callDetails: { - arguments: [] - }, - backtrace: phantomas.getBacktrace() - }); - - }, function(result, args) { - phantomas.leaveContext(); - }); - - - phantomas.spy(window, 'getComputedStyle', function(element, pseudoElement) { - var target = phantomas.getDOMPath(element); - - phantomas.enterContext({ - type: 'getComputedStyle', - callDetails: { - arguments: [target, pseudoElement] - }, - backtrace: phantomas.getBacktrace() - }); - - }, function(result, args) { - phantomas.leaveContext(); - }); - - })(window.__phantomas); - }); - }); - - // report DOM queries that return no results (issue #420) - phantomas.on('domQuery', function(type, query, fnName, context, hasNoResults) { - // ignore DOM queries within DOM fragments (used internally by jQuery) - if (context.indexOf('body') !== 0 && context.indexOf('#document') !== 0) { - return; - } - - if (hasNoResults === true) { - phantomas.incrMetric('DOMqueriesWithoutResults'); - phantomas.addOffender('DOMqueriesWithoutResults', '%s (in %s) using %s', query, context, fnName); - } - }); - - // count DOM queries by either ID, tag name, class name and selector query - // @see https://dvcs.w3.org/hg/domcore/raw-file/tip/Overview.html#dom-document-doctype - var Collection = require('../../util/collection'), - DOMqueries = new Collection(); - - phantomas.on('domQuery', function(type, query, fnName, context) { - phantomas.log('DOM query: by %s - "%s" (using %s) in %s', type, query, fnName, context); - phantomas.incrMetric('DOMqueries'); - - if (context && ( - context.indexOf('html') === 0 || - context.indexOf('body') === 0 || - context.indexOf('head') === 0 || - context.indexOf('#document') === 0 - )) { - DOMqueries.push(type + ' "' + query + '" with ' + fnName + ' (in context ' + context + ')'); - } - }); - - phantomas.on('report', function() { - DOMqueries.sort().forEach(function(query, cnt) { - if (cnt > 1) { - phantomas.incrMetric('DOMqueriesDuplicated'); - phantomas.incrMetric('DOMqueriesAvoidable', cnt - 1); - phantomas.addOffender('DOMqueriesDuplicated', '%s: %d queries', query, cnt); - } - }); - }); -}; \ No newline at end of file diff --git a/lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js b/lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js deleted file mode 100644 index faf3042..0000000 --- a/lib/tools/phantomas/custom_modules/modules/jQYLT/jQYLT.js +++ /dev/null @@ -1,381 +0,0 @@ -/** - * Analyzes jQuery activity - * - * @see http://code.jquery.com/jquery-1.10.2.js - * @see http://code.jquery.com/jquery-2.0.3.js - */ -/* global document: true, window: true */ -/* jshint -W030 */ - -exports.version = '1.0.a'; - -exports.module = function(phantomas) { - 'use strict'; - - phantomas.setMetric('jQueryVersion', ''); // @desc version of jQuery framework (if loaded) [string] - phantomas.setMetric('jQueryVersionsLoaded'); // @desc number of loaded jQuery "instances" (even in the same version) - phantomas.setMetric('jQueryOnDOMReadyFunctions'); // @desc number of functions bound to onDOMReady event - phantomas.setMetric('jQueryWindowOnLoadFunctions'); // @desc number of functions bound to windowOnLoad event - phantomas.setMetric('jQuerySizzleCalls'); // @desc number of calls to Sizzle (including those that will be resolved using querySelectorAll) - phantomas.setMetric('jQueryEventTriggers'); // @desc number of jQuery event triggers - - var jQueryFunctions = [ - // DOM manipulations - 'html', - 'append', - 'appendTo', - 'prepend', - 'prependTo', - 'before', - 'insertBefore', - 'after', - 'insertAfter', - 'remove', - 'detach', - 'empty', - 'clone', - 'replaceWith', - 'replaceAll', - 'text', - 'wrap', - 'wrapAll', - 'wrapInner', - 'unwrap', - - // Style manipulations - 'css', - 'offset', - 'position', - 'height', - 'innerHeight', - 'outerHeight', - 'width', - 'innerWidth', - 'outerWidth', - 'scrollLeft', - 'scrollTop', - - // Animations - 'hide', - 'show', - 'toggle', - 'animate', - 'fadeIn', - 'fadeOut', - 'fadeTo', - 'fadeToggle', - 'slideDown', - 'slideUp', - 'slideToggle', - - // Generic events - 'on', - 'off', - 'live', - 'die', - 'delegate', - 'undelegate', - 'one', - 'bind', - 'unbind', - - // More events - 'blur', - 'change', - 'click', - 'dblclick', - 'error', - 'focus', - 'focusin', - 'focusout', - 'hover', - 'keydown', - 'keypress', - 'keyup', - 'load', - 'mousedown', - 'mouseenter', - 'mouseleave', - 'mousemove', - 'mouseout', - 'mouseover', - 'mouseup', - 'resize', - 'scroll', - 'select', - 'submit', - 'unload', - - // Attributes - 'attr', - 'prop', - 'removeAttr', - 'removeProp', - 'val', - 'hasClass', - 'addClass', - 'removeClass', - 'toggleClass', - ]; - - var jQueryTraversalFunctions = [ - 'children', - 'closest', - 'find', - 'next', - 'nextAll', - 'nextUntil', - 'offsetParent', - 'parent', - 'parents', - 'parentsUntil', - 'prev', - 'prevAll', - 'prevUntil', - 'siblings' - ]; - - jQueryFunctions = jQueryFunctions.concat(jQueryTraversalFunctions); - - // spy calls to jQuery functions - phantomas.on('init', function() { - phantomas.evaluate(function(jQueryFunctions, jQueryTraversalFunctions) { - (function(phantomas) { - var oldJQuery; - - phantomas.spyGlobalVar('jQuery', function(jQuery) { - var version; - - if (!jQuery || !jQuery.fn) { - phantomas.log('jQuery: unable to detect version!'); - return; - } - - // Tag the current version of jQuery to avoid multiple reports of jQuery being loaded - // when it's actually only restored via $.noConflict(true) - see comments in #435 - if (jQuery.__phantomas === true) { - phantomas.log('jQuery: this instance has already been seen by phantomas'); - return; - } - jQuery.__phantomas = true; - - // report the version of jQuery - version = jQuery.fn.jquery; - phantomas.emit('jQueryLoaded', version); - - phantomas.pushContext({ - type: (oldJQuery) ? 'jQuery version change' : 'jQuery loaded', - callDetails: { - arguments: ['version ' + version] - }, - backtrace: phantomas.getBacktrace() - }); - oldJQuery = version; - - // jQuery.ready.promise - // works for jQuery 1.8.0+ (released Aug 09 2012) - phantomas.spy(jQuery.ready, 'promise', function(func) { - phantomas.incrMetric('jQueryOnDOMReadyFunctions'); - phantomas.addOffender('jQueryOnDOMReadyFunctions', phantomas.getCaller(3)); - - phantomas.pushContext({ - type: 'jQuery - onDOMReady', - callDetails: { - arguments: [func] - }, - backtrace: phantomas.getBacktrace() - }); - - }) || phantomas.log('jQuery: can not measure jQueryOnDOMReadyFunctions (jQuery used on the page is too old)!'); - - - // Sizzle calls - jQuery.find - // works for jQuery 1.3+ (released Jan 13 2009) - phantomas.spy(jQuery, 'find', function(selector, context) { - phantomas.incrMetric('jQuerySizzleCalls'); - phantomas.addOffender('jQuerySizzleCalls', '%s (in %s)', selector, (phantomas.getDOMPath(context) || 'unknown')); - - phantomas.enterContext({ - type: 'jQuery - Sizzle call', - callDetails: { - context: { - length: 1, - elements: [phantomas.getDOMPath(context)] - }, - arguments: [selector] - }, - backtrace: phantomas.getBacktrace() - }); - - }, function(result) { - var moreData = { - resultsNumber : (result && result.length) ? result.length : 0 - }; - phantomas.leaveContext(moreData); - }) || phantomas.log('jQuery: can not measure jQuerySizzleCalls (jQuery used on the page is too old)!'); - - - phantomas.spy(jQuery.fn, 'init', function(selector, context) { - if (typeof selector === 'string' && /^#([\w\-]*)$/.exec(selector) !== null && !context) { - - phantomas.enterContext({ - type: 'jQuery - find', - callDetails: { - arguments: [selector] - }, - backtrace: phantomas.getBacktrace() - }); - - } - - }, function(result) { - var data = phantomas.getContextData(); - - if (data.type === 'jQuery - find' && - !data.callDetails.context && - data.callDetails.arguments.length === 1 && - /^#([\w\-]*)$/.exec(data.callDetails.arguments[0]) !== null) { - - var moreData = { - resultsNumber : (result && result.length) ? result.length : 0 - }; - phantomas.leaveContext(moreData); - } - }); - - - if (!jQuery.event) { - phantomas.spy(jQuery.event, 'trigger', function(ev, data, elem) { - var path = phantomas.getDOMPath(elem), - type = ev.type || ev; - - phantomas.log('Event: triggered "%s" on "%s"', type, path); - - phantomas.incrMetric('jQueryEventTriggers'); - phantomas.addOffender('jQueryEventTriggers', '"%s" on "%s"', type, path); - }); - } - - // jQuery events bound to window' onLoad event (#451) - phantomas.spy(jQuery.fn, 'on', function(eventName, func) { - if ((eventName === 'load') && (this[0] === window)) { - phantomas.incrMetric('jQueryWindowOnLoadFunctions'); - phantomas.addOffender('jQueryWindowOnLoadFunctions', phantomas.getCaller(2)); - } - }); - - // Add spys on many jQuery functions - jQueryFunctions.forEach(function(functionName) { - - phantomas.spy(jQuery.fn, functionName, function(args) { - - // Clean args - args = [].slice.call(arguments); - args.forEach(function(arg, index) { - - if (arg instanceof Array) { - arg = '[Array]'; - } - - if (arg instanceof Object) { - - if (arg instanceof jQuery || (arg.jquery && arg.jquery.length > 0)) { - - arg = phantomas.getDOMPath(arg[0]) || 'unknown'; - - } else if (arg instanceof HTMLElement) { - - arg = phantomas.getDOMPath(arg) || 'unknown'; - - } else if (typeof arg === 'function') { - - arg = '(function)'; - - } else { - - try { - - for (var key in arg) { - if (typeof arg[key] === 'function') { - arg[key] = '(function)'; - } - } - - arg = JSON.stringify(arg); - } catch(e) { - arg = '[Object]'; - } - - } - } - - if ((typeof arg === 'string' || arg instanceof String) && arg.length > 200) { - arg = arg.substring(0, 200) + '...'; - } - - if (typeof arg === 'function') { - arg = '(function)'; - } - - if (arg === true) { - arg = 'true'; - } - - if (arg === false) { - arg = 'false'; - } - - if (arg === null) { - arg = 'null'; - } - - if (typeof arg !== 'number' && typeof arg !== 'string' && !(arg instanceof String)) { - arg = 'undefined'; - } - - args[index] = arg; - }); - - var elements = []; - for (var i = 0 ; i < this.length ; i++) { - elements.push(phantomas.getDOMPath(this[i])); - } - - phantomas.enterContext({ - type: 'jQuery - ' + functionName, - callDetails: { - context: { - length: this.length, - elements: elements - }, - arguments: args - }, - backtrace: phantomas.getBacktrace() - }); - - }, function(result) { - if (jQueryTraversalFunctions.indexOf(functionName) >= 0) { - var moreData = { - resultsNumber : (result && result.length) ? result.length : 0 - }; - phantomas.leaveContext(moreData); - } else { - phantomas.leaveContext(); - } - }) || phantomas.log('jQuery: can not track jQuery - ' + functionName + ' (this version of jQuery doesn\'t support it)'); - }); - }); - })(window.__phantomas); - }, jQueryFunctions, jQueryTraversalFunctions); - }); - - - phantomas.on('jQueryLoaded', function(version) { - phantomas.log('jQuery: loaded v' + version); - phantomas.setMetric('jQueryVersion', version); - - // report multiple jQuery "instances" (issue #435) - phantomas.incrMetric('jQueryVersionsLoaded'); - phantomas.addOffender('jQueryVersionsLoaded', 'v%s', version); - }); -}; diff --git a/lib/tools/phantomas/custom_modules/modules/javaScriptBottleYLT/javaScriptBottleYLT.js b/lib/tools/phantomas/custom_modules/modules/javaScriptBottleYLT/javaScriptBottleYLT.js deleted file mode 100644 index 61489af..0000000 --- a/lib/tools/phantomas/custom_modules/modules/javaScriptBottleYLT/javaScriptBottleYLT.js +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Reports the use of functions known to be serious performance bottlenecks in JS - * - * @see http://www.nczonline.net/blog/2013/06/25/eval-isnt-evil-just-misunderstood/ - * @see http://www.quirksmode.org/blog/archives/2005/06/three_javascrip_1.html - * @see http://www.stevesouders.com/blog/2012/04/10/dont-docwrite-scripts/ - * - * Run phantomas with --spy-eval to count eval() calls (see issue #467) - */ -/* global document: true, window: true */ - -exports.version = '0.2'; - -exports.module = function(phantomas) { - 'use strict'; - - phantomas.setMetric('documentWriteCalls'); //@desc number of calls to either document.write or document.writeln @offenders - phantomas.setMetric('evalCalls'); // @desc number of calls to eval (either direct or via setTimeout / setInterval) @offenders - - // spy calls to eval only when requested (issue #467) - var spyEval = phantomas.getParam('spy-eval') === true; - if (!spyEval) { - phantomas.log('javaScriptBottlenecks: to spy calls to eval() run phantomas with --spy-eval option'); - } - - phantomas.on('init', function() { - phantomas.evaluate(function(spyEval) { - (function(phantomas) { - function report(msg, caller, backtrace, metric) { - phantomas.log(msg + ': from ' + caller + '!'); - phantomas.log('Backtrace: ' + backtrace); - - phantomas.incrMetric(metric); - phantomas.addOffender(metric, "%s from %s", msg, caller); - } - - // spy calls to eval() - if (spyEval) { - phantomas.spy(window, 'eval', function(code) { - report('eval() called directly', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls'); - phantomas.log('eval\'ed code: ' + (code || '').substring(0, 150) + '(...)'); - }); - } - - // spy calls to setTimeout / setInterval with string passed instead of a function - phantomas.spy(window, 'setTimeout', function(fn, interval) { - if (typeof fn !== 'string') return; - - report('eval() called via setTimeout("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls'); - }); - - phantomas.spy(window, 'setInterval', function(fn, interval) { - if (typeof fn !== 'string') return; - - report('eval() called via setInterval("' + fn + '")', phantomas.getCaller(), phantomas.getBacktrace(), 'evalCalls'); - }); - - // spy document.write(ln) - phantomas.spy(document, 'write', function(arg) { - report('document.write() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls'); - }); - - phantomas.spy(document, 'writeln', function(arg) { - report('document.writeln() used', phantomas.getCaller(), phantomas.getBacktrace(), 'documentWriteCalls'); - }); - })(window.__phantomas); - }, spyEval); - }); -}; diff --git a/lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js b/lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js deleted file mode 100644 index 697b1db..0000000 --- a/lib/tools/phantomas/custom_modules/modules/jsTreeYLT/jsTreeYLT.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Saves the javascript interractions with the DOM - * - * Run phantomas with --js-execution-tree option to use this module - */ - -exports.version = '0.1'; - -exports.module = function(phantomas) { - 'use strict'; - - phantomas.setMetric('javascriptExecutionTree'); - - // save data - phantomas.on('report', function() { - phantomas.log('JS execution tree: Reading execution tree JSON'); - - phantomas.evaluate(function() {(function(phantomas) { - var fullTree = phantomas.readFullTree(); - - if (fullTree === null) { - phantomas.log('JS execution tree: error, the execution tree is not correctly closed'); - return; - } - - phantomas.setMetric('javascriptExecutionTree', true, true); - phantomas.addOffender('javascriptExecutionTree', JSON.stringify(fullTree)); - - })(window.__phantomas);}); - }); -}; diff --git a/lib/tools/phantomas/custom_modules/modules/scrollListener/scrollListener.js b/lib/tools/phantomas/custom_modules/modules/scrollListener/scrollListener.js deleted file mode 100644 index bc250b0..0000000 --- a/lib/tools/phantomas/custom_modules/modules/scrollListener/scrollListener.js +++ /dev/null @@ -1,69 +0,0 @@ -exports.version = '0.1'; - -exports.module = function(phantomas) { - 'use strict'; - - phantomas.setMetric('scrollExecutionTree'); - - phantomas.on('report', function() { - - phantomas.evaluate(function() { - (function(phantomas) { - - var evt = document.createEvent('CustomEvent'); - evt.initCustomEvent('scroll', false, false, null); - - function triggerScrollEvent() { - phantomas.resetTree(); - - try { - - // Chrome triggers them in this order: - - // 1. document - phantomas.pushContext({ - type: 'documentScroll' - }); - document.dispatchEvent(evt); - - // 2. window - phantomas.pushContext({ - type: 'windowScroll' - }); - window.dispatchEvent(evt); - - // No need to call window.onscroll(), it's called by the scroll event on window - - } catch(e) { - phantomas.log('ScrollListener error: %s', e); - } - } - - var firstScrollTime = Date.now(); - phantomas.log('ScrollListener: triggering a first scroll event...'); - triggerScrollEvent(); - - - // Ignore the first scroll event and only save the second one, - // because we want to detect un-throttled things, throttled ones are ok. - var secondScrollTime = Date.now(); - phantomas.log('ScrollListener: triggering a second scroll event (%dms after the first)...', secondScrollTime - firstScrollTime); - triggerScrollEvent(); - - - var fullTree = phantomas.readFullTree(); - if (fullTree !== null) { - phantomas.setMetric('scrollExecutionTree', true, true); - phantomas.addOffender('scrollExecutionTree', JSON.stringify(fullTree)); - phantomas.log('ScrollListener: scrollExecutionTree correctly extracted'); - } else { - phantomas.log('Error: scrollExecutionTree could not be extracted'); - } - - - phantomas.log('ScrollListener: end of scroll triggering'); - - })(window.__phantomas); - }); - }); -}; diff --git a/lib/tools/phantomas/custom_modules/modules/windowPerfYLT/windowPerfYLT.js b/lib/tools/phantomas/custom_modules/modules/windowPerfYLT/windowPerfYLT.js deleted file mode 100644 index 43082f1..0000000 --- a/lib/tools/phantomas/custom_modules/modules/windowPerfYLT/windowPerfYLT.js +++ /dev/null @@ -1,150 +0,0 @@ -/** - * Measure when the page reaches certain states - * - * @see http://w3c-test.org/webperf/specs/NavigationTiming/#dom-performancetiming-domloading - * @see https://developers.google.com/web/fundamentals/performance/critical-rendering-path/measure-crp - */ -/* global document: true, window: true */ - -exports.version = '1.0.a'; - -exports.module = function(phantomas) { - 'use strict'; - - // times below are calculated relative to performance.timing.responseEnd (#117) - phantomas.setMetric('domInteractive'); // @desc time it took to parse the HTML and construct the DOM - phantomas.setMetric('domContentLoaded'); // @desc time it took to construct both DOM and CSSOM, no stylesheets that are blocking JavaScript execution (i.e. onDOMReady) - phantomas.setMetric('domContentLoadedEnd'); // @desc time it took to finish handling of onDOMReady event @unreliable - phantomas.setMetric('domComplete'); // @desc time it took to load all page resources, the loading spinner has stopped spinning - - // backend vs frontend time - phantomas.setMetric('timeBackend'); // @desc time to the first byte compared to the total loading time [%] - phantomas.setMetric('timeFrontend'); // @desc time to window.load compared to the total loading time [%] - - // measure dom... metrics from the moment HTML response was fully received - var responseEndTime = Date.now(); - - phantomas.on('responseEnd', function() { - responseEndTime = Date.now(); - phantomas.log('Performance timing: responseEnd = %d', responseEndTime); - }); - - phantomas.on('init', function() { - phantomas.evaluate(function(responseEndTime) { - (function(phantomas) { - phantomas.spyEnabled(false, 'installing window.performance metrics'); - - phantomas.currentStep = 'domCreation'; - - // extend window.performance - // "init" event is sometimes fired twice, pass a value set by "responseEnd" event handler (fixes #192) - if (typeof window.performance === 'undefined') { - window.performance = { - timing: { - responseEnd: responseEndTime - } - }; - - phantomas.log('Performance timing: emulating window.performance'); - } - else { - phantomas.log('Performance timing: using native window.performance'); - } - - // onDOMReady - document.addEventListener("DOMContentLoaded", function() { - - setTimeout(function() { - // use NavigationTiming if possible - var time = window.performance.timing.domContentLoadedEventEnd ? - (window.performance.timing.domContentLoadedEventEnd - window.performance.timing.responseEnd) - : - (Date.now() - responseEndTime); - - phantomas.currentStep = 'domContentLoadedEnd'; - phantomas.setMetric('domContentLoadedEnd', time, true); - phantomas.log('Performance timing: document reached "DOMContentLoadedEnd" state after %d ms', time); - - phantomas.pushContext({ - type: 'domContentLoadedEnd' - }); - }, 0); - - var time = Date.now() - responseEndTime; - - phantomas.currentStep = 'domContentLoaded'; - phantomas.setMetric('domContentLoaded', time, true); - phantomas.log('Performance timing: document reached "DOMContentLoaded" state after %d ms', time); - - phantomas.pushContext({ - type: 'domContentLoaded' - }); - }); - - // emulate Navigation Timing - document.addEventListener('readystatechange', function() { - var readyState = document.readyState, - responseEndTime = window.performance.timing.responseEnd, - time = Date.now() - responseEndTime, - metricName; - - // @see http://www.w3.org/TR/html5/dom.html#documentreadystate - switch(readyState) { - // the browser has finished parsing all of the HTML and DOM construction is complete - case 'interactive': - metricName = 'domInteractive'; - break; - - // the processing is complete and all of the resources on the page have finished downloading - case 'complete': - metricName = 'domComplete'; - phantomas.log('Performance timing: %j', window.performance.timing); - break; - - default: - phantomas.log('Performance timing: unhandled "%s" state!', readyState); - return; - } - - phantomas.currentStep = metricName; - phantomas.setMetric(metricName, time, true); - phantomas.log('Performance timing: document reached "%s" state after %d ms', readyState, time); - - phantomas.pushContext({ - type: metricName - }); - }); - - phantomas.spyEnabled(true); - })(window.__phantomas); - }, responseEndTime); - }); - - /** - * Emit metrics with backend vs frontend time - * - * Performance Golden Rule: - * "80-90% of the end-user response time is spent on the frontend. Start there." - * - * @see http://www.stevesouders.com/blog/2012/02/10/the-performance-golden-rule/ - */ - phantomas.on('report', function() { - // The “backend” time is the time it takes the server to get the first byte back to the client. - // The “frontend” time is measured from the last byte of the response (responseEnd) until all resources are fetched (domComplete) - var backendTime = parseInt(phantomas.getMetric('timeToFirstByte'), 10), - frontendTime = parseInt(phantomas.getMetric('domComplete'), 10), - totalTime = backendTime + frontendTime, - backendTimePercentage; - - if (totalTime === 0) { - return; - } - - backendTimePercentage = Math.round(backendTime / totalTime * 100); - - phantomas.setMetric('timeBackend', backendTimePercentage); - phantomas.setMetric('timeFrontend', 100 - backendTimePercentage); - - phantomas.log('Performance timing: backend vs frontend time - %d% / %d%', backendTimePercentage, 100 - backendTimePercentage); - }); -}; From 517206364a9f73fd6c43f07ff8d0e261fabd90d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Thu, 14 Mar 2019 03:27:37 +0100 Subject: [PATCH 03/59] Work in progress: switch to Phantomas v2 --- .travis.yml | 1 - Gruntfile.js | 7 +- lib/offendersHelpers.js | 26 ++--- lib/runner.js | 6 - lib/tools/phantomas/phantomasWrapper.js | 121 ++++++++------------- lib/tools/redownload/contentTypeChecker.js | 48 ++++++++ lib/tools/redownload/redownload.js | 75 +++++++++++-- package.json | 5 +- test/api/apiTest.js | 33 ------ test/core/indexTest.js | 11 -- test/core/mediaQueriesCheckerTest.js | 94 +++++++++------- test/core/offendersHelpersTest.js | 46 -------- test/core/phantomasWrapperTest.js | 5 +- test/core/redownloadTest.js | 89 +++------------ 14 files changed, 248 insertions(+), 319 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3718230..c14434c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,6 @@ language: node_js sudo: false node_js: - "8.9" - - "6.2" env: - CXX=g++-4.8 addons: diff --git a/Gruntfile.js b/Gruntfile.js index 90eb3d2..4d5aea4 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -71,7 +71,10 @@ module.exports = function(grunt) { 'test/core/*.js', 'test/fixtures/*.js', 'front/src/js/**/*.js' - ] + ], + options: { + esversion: 6 + } }, clean: { tmp: { @@ -103,7 +106,7 @@ module.exports = function(grunt) { options: { reporter: 'spec', }, - src: ['test/api/apiTest.js'] + src: ['test/core/mediaQueriesCheckerTest.js'] } }, env: { diff --git a/lib/offendersHelpers.js b/lib/offendersHelpers.js index 482deaf..f2e9143 100644 --- a/lib/offendersHelpers.js +++ b/lib/offendersHelpers.js @@ -172,23 +172,15 @@ var OffendersHelpers = function() { }; this.cssOffenderPattern = function(offender) { - // Remove any line breaks - offender = offender.replace(/(\r\n|\n|\r)/gm, ''); - - var parts = /^(.*) (?:<([^ \(]*)>|\[inline CSS\]) ?@ ?(\d+):(\d+)$/.exec(offender); - - if (!parts) { - return { - offender: offender - }; - } else { - return { - css: parts[1], - file: parts[2] || null, - line: parseInt(parts[3], 10), - column: parseInt(parts[4], 10) - }; - } + // Used to work with strings + // As of Phantomas v2, offender is now in JSON format. + // Let's just adapt this for now and we'll see later if we remove completely this function + return { + css: offender.value.message, + file: offender.url, + line: offender.value.position.start.line, + column: offender.value.position.start.column + }; }; this.fileWithSizePattern = function(fileWithSize) { diff --git a/lib/runner.js b/lib/runner.js index aae7275..c201302 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -56,12 +56,6 @@ var Runner = function(params) { generic : scoreCalculator.calculate(data, scoreProfileGeneric) }; - - delete data.toolsResults.phantomas.metrics.javascriptExecutionTree; - delete data.toolsResults.phantomas.offenders.javascriptExecutionTree; - delete data.toolsResults.phantomas.metrics.scrollExecutionTree; - delete data.toolsResults.phantomas.offenders.scrollExecutionTree; - if (data.toolsResults.phantomas.offenders.blockedRequests) { data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests; diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index e27e37f..8bd934f 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -87,91 +87,55 @@ var PhantomasWrapper = function() { debug('node node_modules/phantomas/bin/phantomas.js --url=' + task.url + optionsString + ' --verbose'); - var phantomasPid; - var isKilled = false; - - // Kill phantomas if nothing happens - var killer = setTimeout(function() { - console.log('Killing phantomas because the test on ' + task.url + ' was launched ' + 5*options.timeout + ' seconds ago'); - - if (phantomasPid) { - ps.kill(phantomasPid, function(err) { - - if (err) { - debug('Could not kill Phantomas process %s', phantomasPid); - - // Suicide - process.exit(1); - // If in server mode, forever will restart the server - } - - debug('Phantomas process %s was correctly killed', phantomasPid); - - // Then mark the test as failed - // Error 1003 = Phantomas not answering - deferred.reject(1003); - isKilled = true; - }); - } else { - // Suicide - process.exit(1); - } - - }, 5*options.timeout*1000); - - // It's time to launch the test!!! - var triesNumber = 2; - var currentTry = 0; + + const promise = phantomas(task.url, { + 'analyze-css': true + }); - async.retry(triesNumber, function(cb) { + // handle the promise + promise. + then(results => { + var json = { + generator: results.getGenerator(), + url: results.getUrl(), + metrics: results.getMetrics(), + offenders: results.getAllOffenders() + }; - currentTry ++; - - var process = phantomas(task.url, options, function(err, json, results) { - var errorCode = err ? parseInt(err.message, 10) : null; - - if (isKilled) { - debug('Process was killed, too late Phantomas, sorry...'); - return; - } - - - debug('Returning from Phantomas with error %s', errorCode); - - // Adding some YellowLabTools errors here - if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) { - errorCode = 1001; - } - - if (!errorCode && (!json || !json.metrics)) { - errorCode = 1002; - } - - // Don't cancel test if it is a timeout and we've got some results - if (errorCode === 252 && json) { - debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.'); - errorCode = null; - } - - if (errorCode) { - debug('Attempt failed. Error code ' + errorCode); - } - - cb(errorCode, json); - - }).fail(function() { - // This function is useless, but the failing promise needs to be handled, - // otherwise the module meow writes in the console in case of a timeout (error code 252). - debug('Failing promise handled'); + deferred.resolve(json); + }). + catch(res => { + console.error(res); + deferred.reject('Phantomas failed: ' + res.message); }); - - phantomasPid = process.pid; + + /*var process = phantomas(task.url, options, function(err, json, results) { + var errorCode = err ? parseInt(err.message, 10) : null; + + debug('Returning from Phantomas with error %s', errorCode); + + // Adding some YellowLabTools errors here + if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) { + errorCode = 1001; + } + + if (!errorCode && (!json || !json.metrics)) { + errorCode = 1002; + } + + // Don't cancel test if it is a timeout and we've got some results + if (errorCode === 252 && json) { + debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.'); + errorCode = null; + } + + if (errorCode) { + debug('Attempt failed. Error code ' + errorCode); + } }, function(err, json) { - clearTimeout(killer); - if (err) { debug('All ' + triesNumber + ' attemps failed for the test'); deferred.reject(err); @@ -182,6 +146,7 @@ var PhantomasWrapper = function() { } }); + */ return deferred.promise; }; diff --git a/lib/tools/redownload/contentTypeChecker.js b/lib/tools/redownload/contentTypeChecker.js index 49d242b..a55a536 100644 --- a/lib/tools/redownload/contentTypeChecker.js +++ b/lib/tools/redownload/contentTypeChecker.js @@ -4,6 +4,7 @@ var isJpg = require('is-jpg'); var isPng = require('is-png'); var isSvg = require('is-svg'); var isGif = require('is-gif'); +var isWebp = require('is-webp'); var isWoff = require('is-woff'); var isWoff2 = require('is-woff2'); var isOtf = require('is-otf'); @@ -17,6 +18,39 @@ var ContentTypeChecker = function() { debug('Entering contentTypeChecker'); + // Setting isSomething values: + switch(entry.type) { + case 'html': + entry.isHTML = true; + break; + case 'xml': + entry.isXML = true; + break; + case 'css': + entry.isCSS = true; + break; + case 'js': + entry.isJS = true; + break; + case 'json': + entry.isJSON = true; + break; + case 'image': + entry.isImage = true; + break; + case 'webfont': + entry.isWebFont = true; + break; + case 'video': + entry.isVideo = true; + break; + case 'favicon': + entry.isFavicon = true; + break; + } + + // Now let's check for mistakes by analysing body content. It happens more often then we think! + // Ignore very small files as they are generally tracking pixels if (entry.weightCheck && entry.weightCheck.bodyBuffer && entry.weightCheck.bodySize > 100) { var foundType; @@ -24,6 +58,9 @@ var ContentTypeChecker = function() { try { foundType = findContentType(entry.weightCheck.bodyBuffer); + // Note: as of Phantomas v2, entry.contentType is always undefined + // It could change, so let's keep the following code in place: + if (!entry.contentType || entry.contentType === '') { if (foundType === null) { debug('ContentType is empty for file %s', entry.url); @@ -71,6 +108,10 @@ var ContentTypeChecker = function() { return contentTypes.gif; } + if (isWebp(bodyBuffer)) { + return contentTypes.webp; + } + if (isWoff(bodyBuffer)) { return contentTypes.woff; } @@ -142,6 +183,13 @@ var ContentTypeChecker = function() { entry.isImage = true; } }, + webp: { + mimes: ['image/webp'], + updateFn: function(entry) { + entry.type = 'image'; + entry.isImage = true; + } + }, woff: { mimes: ['application/x-font-woff', 'application/font-woff', 'font/woff'], updateFn: function(entry) { diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index 9c5b04e..2b49753 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -34,9 +34,13 @@ var Redownload = function() { debug('Redownload started'); var deferred = Q.defer(); - var requestsList = JSON.parse(data.toolsResults.phantomas.offenders.requestsList); - delete data.toolsResults.phantomas.metrics.requestsList; - delete data.toolsResults.phantomas.offenders.requestsList; + var requestsOffenders = data.toolsResults.phantomas.offenders.requests; + var gzipOffenders = data.toolsResults.phantomas.offenders.gzipRequests; + var postOffenders = data.toolsResults.phantomas.offenders.postRequests; + var notFoundOffenders = data.toolsResults.phantomas.offenders.notFound; + var redirectOffenders = data.toolsResults.phantomas.offenders.redirects; + + var requestsList = mergeOffenders(requestsOffenders, gzipOffenders, postOffenders, notFoundOffenders, redirectOffenders); var httpAuth = null; if (data.params && data.params.options && data.params.options.authUser && data.params.options.authPass) { @@ -194,6 +198,57 @@ var Redownload = function() { return deferred.promise; } + function mergeOffenders(requests, compressedOffenders, postOffenders, notFoundOffenders, redirectOffenders) { + + // Parse each request and check if it can be found in other offenders + requests.forEach(function(request) { + + // Is it compressed? + if (compressedOffenders) { + compressedOffenders.some(function(entry) { + if (entry.url === request.url) { + request.compressed = true; + request.bodySize = entry.bodySize; + request.transferedSize = entry.transferedSize; + return true; + } + }); + } + + // Is it a POST request? + if (postOffenders) { + postOffenders.some(function(url) { + if (url === request.url) { + request.post = true; + return true; + } + }); + } + + // Is it a 404? + if (notFoundOffenders) { + notFoundOffenders.some(function(url) { + if (url === request.url) { + request.notFound = true; + return true; + } + }); + } + + // Is it a redirection? + if (redirectOffenders) { + redirectOffenders.some(function(message) { + if (message.split(' ')[0] === request.url) { + request.redirect = true; + return true; + } + }); + } + }); + + return requests; + } + function listIncorrectContentTypes(requests) { var results = []; @@ -639,13 +694,15 @@ var Redownload = function() { deferred.resolve(entry); } - if (entry.method !== 'GET') { + if (entry.post) { notDownloadableFile('only downloading GET'); + // ... at least trying to return deferred.promise; } - if (entry.status !== 200) { + if (entry.notFound || entry.redirect) { unwantedFile('only downloading requests with status code 200'); + // ...at least trying to return deferred.promise; } @@ -654,12 +711,12 @@ var Redownload = function() { return deferred.promise; } - debug('Downloading %s', entry.url); - // Always add a gzip header before sending, in case the server listens to it - var reqHeaders = entry.requestHeaders; - reqHeaders['Accept-Encoding'] = 'gzip, deflate'; + // Always add gzip and webp headers before sending, in case the server listens to them + var reqHeaders = []; + reqHeaders['Accept'] = '*/*,image/webp'; + reqHeaders['Accept-Encoding'] = 'gzip, deflate, br'; reqHeaders['Connection'] = 'keep-alive'; var requestOptions = { diff --git a/package.json b/package.json index d168c9d..b93711d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "yellowlabtools": "./bin/cli.js" }, "engines": { - "node": ">= 6.0" + "node": ">= 8.0" }, "main": "./lib/index.js", "dependencies": { @@ -54,6 +54,7 @@ "is-png": "1.1.0", "is-svg": "3.0.0", "is-ttf": "0.2.2", + "is-webp": "1.0.1", "is-woff": "1.0.3", "is-woff2": "1.0.0", "jimp": "0.6.0", @@ -61,7 +62,7 @@ "meow": "5.0.0", "minimize": "2.2.0", "parse-color": "1.0.0", - "phantomas": "1.19.0", + "phantomas": "git://github.com/macbre/phantomas.git#phantomas-v2", "ps-node": "0.1.6", "q": "1.5.1", "request": "2.88.0", diff --git a/test/api/apiTest.js b/test/api/apiTest.js index 7d9d6d5..88daaf8 100644 --- a/test/api/apiTest.js +++ b/test/api/apiTest.js @@ -171,9 +171,6 @@ describe('api', function() { body.should.have.a.property('rules').that.is.an('object'); body.should.have.a.property('toolsResults').that.is.an('object'); - body.should.have.a.property('javascriptExecutionTree').that.is.an('object'); - body.javascriptExecutionTree.should.not.deep.equal({}); - // Check if settings are correctly sent and retrieved body.params.options.should.have.a.property('device').that.equals('tablet'); //body.params.options.should.have.a.property('waitForSelector').that.equals('*'); @@ -410,10 +407,6 @@ describe('api', function() { body.should.have.a.property('toolsResults').that.is.an('object'); body.toolsResults.should.have.a.property('phantomas').that.is.an('object'); - body.should.have.a.property('javascriptExecutionTree').that.is.an('object'); - body.javascriptExecutionTree.should.have.a.property('data').that.is.an('object'); - body.javascriptExecutionTree.data.should.have.a.property('type').that.equals('main'); - done(); } else { @@ -507,28 +500,6 @@ describe('api', function() { }); - it('should return the javascript execution tree', function(done) { - this.timeout(5000); - - request({ - method: 'GET', - url: serverUrl + '/api/results/' + asyncRunId + '/javascriptExecutionTree', - json: true, - }, function(error, response, body) { - if (!error && response.statusCode === 200) { - - body.should.have.a.property('data').that.is.an('object'); - body.data.should.have.a.property('type').that.equals('main'); - - done(); - - } else { - done(error || response.statusCode); - } - }); - }); - - it('should return the phantomas results', function(done) { this.timeout(5000); @@ -565,7 +536,6 @@ describe('api', function() { body.should.have.a.property('params').that.is.an('object'); body.should.have.a.property('scoreProfiles').that.is.an('object'); body.should.have.a.property('rules').that.is.an('object'); - body.should.have.a.property('javascriptExecutionTree').that.is.an('object'); body.should.not.have.a.property('toolsResults').that.is.an('object'); @@ -591,7 +561,6 @@ describe('api', function() { body.should.have.a.property('runId').that.equals(asyncRunId); body.should.have.a.property('scoreProfiles').that.is.an('object'); body.should.have.a.property('rules').that.is.an('object'); - body.should.have.a.property('javascriptExecutionTree').that.is.an('object'); body.should.not.have.a.property('params').that.is.an('object'); body.should.not.have.a.property('toolsResults').that.is.an('object'); @@ -617,7 +586,6 @@ describe('api', function() { body.should.have.a.property('runId').that.equals(asyncRunId); body.should.have.a.property('scoreProfiles').that.is.an('object'); body.should.have.a.property('rules').that.is.an('object'); - body.should.have.a.property('javascriptExecutionTree').that.is.an('object'); body.should.have.a.property('params').that.is.an('object'); body.should.have.a.property('toolsResults').that.is.an('object'); @@ -642,7 +610,6 @@ describe('api', function() { body.should.have.a.property('runId').that.equals(asyncRunId); body.should.have.a.property('scoreProfiles').that.is.an('object'); body.should.have.a.property('rules').that.is.an('object'); - body.should.have.a.property('javascriptExecutionTree').that.is.an('object'); body.should.have.a.property('params').that.is.an('object'); body.should.have.a.property('toolsResults').that.is.an('object'); diff --git a/test/core/indexTest.js b/test/core/indexTest.js index 71874d9..68f5196 100644 --- a/test/core/indexTest.js +++ b/test/core/indexTest.js @@ -87,17 +87,6 @@ describe('index.js', function() { } }); - // Test javascriptExecutionTree - data.toolsResults.phantomas.metrics.should.not.have.a.property('javascriptExecutionTree'); - data.toolsResults.phantomas.offenders.should.not.have.a.property('javascriptExecutionTree'); - data.should.have.a.property('javascriptExecutionTree').that.is.an('object'); - data.javascriptExecutionTree.should.have.a.property('data'); - data.javascriptExecutionTree.data.should.have.a.property('type').that.equals('main'); - data.javascriptExecutionTree.data.should.have.a.property('domInteractive').that.is.a('number'); - data.javascriptExecutionTree.data.should.have.a.property('domContentLoaded').that.is.a('number'); - data.javascriptExecutionTree.data.should.have.a.property('domContentLoadedEnd').that.is.a('number'); - data.javascriptExecutionTree.data.should.have.a.property('domComplete').that.is.a('number'); - /*jshint expr: true*/ console.log.should.not.have.been.called; diff --git a/test/core/mediaQueriesCheckerTest.js b/test/core/mediaQueriesCheckerTest.js index 7b3222b..2265921 100644 --- a/test/core/mediaQueriesCheckerTest.js +++ b/test/core/mediaQueriesCheckerTest.js @@ -2,54 +2,74 @@ var should = require('chai').should(); var mediaQueriesChecker = require('../../lib/tools/mediaQueriesChecker'); describe('mediaQueriesChecker', function() { + + function wrap(message) { + return { + url: 'http://domain.com/css/stylesheet.css', + value: { + message: message, + position: { + start: { + line: 269, + column: 1 + }, + end: { + line: 269, + column: 182 + } + } + } + }; + } it('should parse mediaQueryes correctly', function() { - mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) @ 269:1').should.deep.equal({ - mediaQuery: { - query: 'screen and (max-width: 1024px)', - rules: 1, - file: 'http://domain.com/css/stylesheet.css', - line: 269, - column: 1 - }, - isForMobile: true, - breakpoints: [{string: '1024px', pixels: 1024}] - }); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 1024px) (1 rules)')) + .should.deep.equal({ + mediaQuery: { + query: 'screen and (max-width: 1024px)', + rules: 1, + file: 'http://domain.com/css/stylesheet.css', + line: 269, + column: 1 + }, + isForMobile: true, + breakpoints: [{string: '1024px', pixels: 1024}] + }); }); it('should determine if it\'s for mobile correctly', function() { - mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) @ 1:1').isForMobile.should.equal(true); - mediaQueriesChecker.parseOneMediaQuery('@media (max-width:1024px) (1 rules) @ 1:1').isForMobile.should.equal(true); - mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 320px) (1 rules) @ 1:1').isForMobile.should.equal(true); - mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 290px) (1 rules) @ 1:1').isForMobile.should.equal(false); - mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 320px) (1 rules) @ 1:1').isForMobile.should.equal(false); - mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 600px) (1 rules) @ 1:1').isForMobile.should.equal(false); - mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 20em) (1 rules) @ 1:1').isForMobile.should.equal(true); - mediaQueriesChecker.parseOneMediaQuery('@media screen and (min-width: 40em) (1 rules) @ 1:1').isForMobile.should.equal(false); - mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 600px) and (max-width: 1000px) (1 rules) @ 1:1').isForMobile.should.equal(false); - mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 180px) and (max-width: 400px) (1 rules) @ 1:1').isForMobile.should.equal(true); - mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 180px) and (max-width: 290px) (1 rules) @ 1:1').isForMobile.should.equal(false); - mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 600px) (1 rules) @ 1:1').isForMobile.should.equal(true); - mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 180px) (1 rules) @ 1:1').isForMobile.should.equal(false); - mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules) @ 1:1').isForMobile.should.equal(false); - mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 400px) and (max-width: 180px) (1 rules) @ 1:1').isForMobile.should.equal(true); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 1024px) (1 rules)')).isForMobile.should.equal(true); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media (max-width:1024px) (1 rules)')).isForMobile.should.equal(true); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 320px) (1 rules)')).isForMobile.should.equal(true); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 290px) (1 rules)')).isForMobile.should.equal(false); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (min-width: 320px) (1 rules)')).isForMobile.should.equal(false); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (min-width: 600px) (1 rules)')).isForMobile.should.equal(false); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 20em) (1 rules)')).isForMobile.should.equal(true); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (min-width: 40em) (1 rules)')).isForMobile.should.equal(false); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media (min-width: 600px) and (max-width: 1000px) (1 rules)')).isForMobile.should.equal(false); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media (min-width: 180px) and (max-width: 400px) (1 rules)')).isForMobile.should.equal(true); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media (min-width: 180px) and (max-width: 290px) (1 rules)')).isForMobile.should.equal(false); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 600px) (1 rules)')).isForMobile.should.equal(true); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 180px) (1 rules)')).isForMobile.should.equal(false); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules)')).isForMobile.should.equal(false); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 400px) and (max-width: 180px) (1 rules)')).isForMobile.should.equal(true); }); it('should count breakpoints correctly', function() { - mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 1024px) (1 rules) @ 1:1').breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]); - mediaQueriesChecker.parseOneMediaQuery('@media (max-width:1024px) (1 rules) @ 1:1').breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]); - mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 320px) (1 rules) @ 1:1').breakpoints.should.deep.equal([{string: '320px', pixels: 320}]); - mediaQueriesChecker.parseOneMediaQuery('@media (min-width: 600px) and (max-width: 1000px) (1 rules) @ 1:1').breakpoints.should.deep.equal([{string: '600px', pixels: 600}, {string: '1000px', pixels: 1000}]); - mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 180px) (1 rules) @ 1:1').breakpoints.should.deep.equal([{string: '180px', pixels: 180}]); - mediaQueriesChecker.parseOneMediaQuery('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules) @ 1:1').breakpoints.should.deep.equal([{string: '1000px', pixels: 1000}, {string: '600px', pixels: 600}]); - mediaQueriesChecker.parseOneMediaQuery('@media (max-height:500px) (1 rules) @ 1:1').breakpoints.should.deep.equal([]); - mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 100em) (1 rules) @ 1:1').breakpoints.should.deep.equal([{string: '100em', pixels: 1600}]); - mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 120pt) (1 rules) @ 1:1').breakpoints.should.deep.equal([{string: '120pt', pixels: 160}]); - mediaQueriesChecker.parseOneMediaQuery('@media screen and (max-width: 40.2em) (1 rules) @ 1:1').breakpoints.should.deep.equal([{string: '40.2em', pixels: 643.2}]); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 1024px) (1 rules)')).breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media (max-width:1024px) (1 rules)')).breakpoints.should.deep.equal([{string: '1024px', pixels: 1024}]); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 320px) (1 rules)')).breakpoints.should.deep.equal([{string: '320px', pixels: 320}]); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media (min-width: 600px) and (max-width: 1000px) (1 rules)')).breakpoints.should.deep.equal([{string: '600px', pixels: 600}, {string: '1000px', pixels: 1000}]); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 180px) (1 rules)')).breakpoints.should.deep.equal([{string: '180px', pixels: 180}]); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media not all and (min-width: 1000px) and (max-width: 600px) (1 rules)')).breakpoints.should.deep.equal([{string: '1000px', pixels: 1000}, {string: '600px', pixels: 600}]); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media (max-height:500px) (1 rules)')).breakpoints.should.deep.equal([]); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 100em) (1 rules)')).breakpoints.should.deep.equal([{string: '100em', pixels: 1600}]); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 120pt) (1 rules)')).breakpoints.should.deep.equal([{string: '120pt', pixels: 160}]); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media screen and (max-width: 40.2em) (1 rules)')).breakpoints.should.deep.equal([{string: '40.2em', pixels: 643.2}]); }); it('should fail silently', function() { - mediaQueriesChecker.parseOneMediaQuery('@media bad syntax (1 rules) @ 1:1').isForMobile.should.equal(false); + mediaQueriesChecker.parseOneMediaQuery(wrap('@media bad syntax (1 rules)')).isForMobile.should.equal(false); }); }); diff --git a/test/core/offendersHelpersTest.js b/test/core/offendersHelpersTest.js index e054840..a01d67c 100644 --- a/test/core/offendersHelpersTest.js +++ b/test/core/offendersHelpersTest.js @@ -307,52 +307,6 @@ describe('offendersHelpers', function() { }); - - describe('cssOffenderPattern', function() { - - it('should transform a css offender into an object', function() { - var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li @ 30:31862'); - - result.should.deep.equal({ - css: '.pagination .plus ul li', - file: 'http://www.pouet.com/css/main.css', - line: 30, - column: 31862 - }); - }); - - it('should work with an inline css', function() { - var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li [inline CSS] @ 1:32'); - - result.should.deep.equal({ - css: '.pagination .plus ul li', - file: null, - line: 1, - column: 32 - }); - }); - - it('should handle the case where line and char are not here', function() { - var result = offendersHelpers.cssOffenderPattern('.pagination .plus ul li'); - - result.should.deep.equal({ - offender: '.pagination .plus ul li' - }); - }); - - it('should handle line breaks inside the string', function() { - var result = offendersHelpers.cssOffenderPattern('.card-mask-wrap { -moz-transform: translate3d(0, \n-288px\n, 0) } // was required by firefox 15 and earlier [inline CSS] @ 29:3'); - - result.should.deep.equal({ - css: '.card-mask-wrap { -moz-transform: translate3d(0, -288px, 0) } // was required by firefox 15 and earlier', - file: null, - line: 29, - column: 3 - }); - }); - - }); - describe('fileWithSizePattern', function() { it('should return an object', function() { diff --git a/test/core/phantomasWrapperTest.js b/test/core/phantomasWrapperTest.js index 1be8a88..3be60df 100644 --- a/test/core/phantomasWrapperTest.js +++ b/test/core/phantomasWrapperTest.js @@ -26,7 +26,6 @@ describe('phantomasWrapper', function() { data.should.have.a.property('url').that.equals(url); data.should.have.a.property('metrics').that.is.an('object').not.empty; data.should.have.a.property('offenders').that.is.an('object').not.empty; - data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty; done(); }).fail(function(err) { @@ -34,7 +33,7 @@ describe('phantomasWrapper', function() { }); }); - it('should fail with error 254', function(done) { + it('should fail when page cannot be fetched', function(done) { var url = 'http://localhost:8389/not-existing.html'; this.timeout(15000); @@ -52,8 +51,8 @@ describe('phantomasWrapper', function() { }).fail(function(err) { try { + console.log(err); should.exist(err); - err.should.equal(254); done(); } catch(error) { diff --git a/test/core/redownloadTest.js b/test/core/redownloadTest.js index a3c048a..9c200be 100644 --- a/test/core/redownloadTest.js +++ b/test/core/redownloadTest.js @@ -10,101 +10,42 @@ describe('redownload', function() { var requestsList = [ { - method: 'GET', url: 'http://localhost:8388/simple-page.html', - requestHeaders: { - 'User-Agent': 'something', - Referer: 'http://www.google.fr/', - Accept: '*/*' - }, - status: 200, - isHTML: true, - type: 'html' + type: 'html', + size: 30000 }, { - method: 'GET', url: 'http://localhost:8388/jquery1.8.3.js', - requestHeaders: { - 'User-Agent': 'something', - Referer: 'http://www.google.fr/', - Accept: '*/*' - }, - status: 200, - isJS: true, - type: 'js' + type: 'js', + size: 30000 }, { - method: 'GET', url: 'http://localhost:8388/jpeg-image.jpg', - requestHeaders: { - 'User-Agent': 'something', - Referer: 'http://www.google.fr/', - Accept: '*/*' - }, - status: 200, - isImage: true, type: 'image', - contentType: 'image/jpeg' + size: 30000 }, { - method: 'GET', url: 'http://localhost:8388/svg-image.svg', - requestHeaders: { - 'User-Agent': 'something', - Referer: 'http://www.google.fr/', - Accept: '*/*' - }, - status: 200, - isImage: true, - isSVG: true, type: 'image', - contentType: 'image/svg+xml' + size: 30000 }, { - method: 'GET', url: 'http://localhost:8388/unminified-script.js', - requestHeaders: { - 'User-Agent': 'something', - Referer: 'http://www.google.fr/', - Accept: '*/*' - }, - status: 200, - isJS: true, - type: 'js' + type: 'js', + size: 30000 }, { - method: 'GET', url: 'http://localhost:8388/unminified-stylesheet.css', - requestHeaders: { - 'User-Agent': 'something', - Referer: 'http://www.google.fr/', - Accept: '*/*' - }, - status: 200, - isCSS: true, - type: 'css' + type: 'css', + size: 30000 }, { - method: 'GET', url: 'http://localhost:8388/xml.xml', - requestHeaders: { - 'User-Agent': 'something', - Referer: 'http://www.google.fr/', - Accept: '*/*' - }, - status: 200, - isXML: true, - type: 'xml' + type: 'xml', + size: 30000 }, { - method: 'GET', - url: 'about:blank', - requestHeaders: { - 'User-Agent': 'something', - Referer: 'http://www.google.fr/', - Accept: '*/*' - }, - status: 200 + url: 'about:blank' } ]; @@ -117,10 +58,10 @@ describe('redownload', function() { toolsResults: { phantomas: { metrics: { - requestsList: true + requests: 8 }, offenders: { - requestsList: JSON.stringify(requestsList) + requests: requestsList } } } From 9bfc16a4d52dc3bf610d119ae9c624538da4788f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 9 Aug 2020 01:13:09 +0200 Subject: [PATCH 04/59] Get rid of the icons font --- Gruntfile.js | 21 ---- front/src/css/dashboard.css | 15 ++- front/src/css/icons.css | 67 ----------- front/src/css/index.css | 12 +- front/src/css/main.css | 109 +++++++----------- front/src/fonts/svg-icons/arrow-left3.svg | 1 - front/src/fonts/svg-icons/bars.svg | 1 - front/src/fonts/svg-icons/lab.svg | 1 - front/src/fonts/svg-icons/list.svg | 1 - front/src/fonts/svg-icons/loop.svg | 1 - front/src/fonts/svg-icons/mobile.svg | 1 - front/src/fonts/svg-icons/question.svg | 1 - front/src/fonts/svg-icons/screen.svg | 1 - front/src/fonts/svg-icons/tablet.svg | 1 - front/src/fonts/svg-icons/warning.svg | 1 - front/src/less/dashboard.less | 14 ++- front/src/less/icons.less | 132 ---------------------- front/src/less/index.less | 16 ++- front/src/less/main.less | 48 +++++++- front/src/main.html | 2 +- front/src/views/dashboard.html | 8 +- front/src/views/index.html | 6 +- front/src/views/resultSubHeader.html | 6 +- package.json | 1 - 24 files changed, 130 insertions(+), 337 deletions(-) delete mode 100644 front/src/css/icons.css delete mode 100644 front/src/fonts/svg-icons/arrow-left3.svg delete mode 100644 front/src/fonts/svg-icons/bars.svg delete mode 100644 front/src/fonts/svg-icons/lab.svg delete mode 100644 front/src/fonts/svg-icons/list.svg delete mode 100755 front/src/fonts/svg-icons/loop.svg delete mode 100755 front/src/fonts/svg-icons/mobile.svg delete mode 100644 front/src/fonts/svg-icons/question.svg delete mode 100755 front/src/fonts/svg-icons/screen.svg delete mode 100755 front/src/fonts/svg-icons/tablet.svg delete mode 100644 front/src/fonts/svg-icons/warning.svg delete mode 100644 front/src/less/icons.less diff --git a/Gruntfile.js b/Gruntfile.js index 4d5aea4..d66c14c 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -11,21 +11,6 @@ module.exports = function(grunt) { pkg: grunt.file.readJSON('package.json'), settings: grunt.file.readJSON('./server_config/settings.json'), - webfont: { - icons: { - src: 'front/src/fonts/svg-icons/*.svg', - dest: 'tmp', - destCss: 'front/src/less', - options: { - engine: 'node', - types: 'woff', - stylesheet: 'less', - embed: true, - htmlDemo: false, - syntax: 'bootstrap' - } - } - }, less: { all: { files: [ @@ -225,12 +210,6 @@ module.exports = function(grunt) { process.env.IS_TEST = true; }); - grunt.registerTask('icons', [ - 'webfont:icons', - 'less', - 'clean:tmp' - ]); - grunt.registerTask('build', [ 'jshint', 'clean:build', diff --git a/front/src/css/dashboard.css b/front/src/css/dashboard.css index c5e089a..f070c80 100644 --- a/front/src/css/dashboard.css +++ b/front/src/css/dashboard.css @@ -120,8 +120,8 @@ .summary .notations .criteria .table > a:hover > div.info { background: #FFF; } -.summary .notations .criteria .table > a:hover > div.info .icon-question { - color: #EBD8E2; +.summary .notations .criteria .table > a:hover > div.info svg { + fill: #EBD8E2; } .summary .notations .criteria .grade { width: 10%; @@ -140,10 +140,13 @@ vertical-align: middle; } .summary .notations .warning .label, -.summary .notations .warning .result, -.summary .notations .icon-warning { +.summary .notations .warning .result { color: #FF1919; } +.summary .notations .icon-warning svg { + fill: #FF1919; + margin: -2px 0; +} .summary .notations .criteria .info { width: 2%; text-align: center; @@ -152,8 +155,8 @@ padding-left: 0.1em; padding-right: 0.1em; } -.summary .notations .criteria .icon-question { - color: transparent; +.summary .notations .criteria .info svg { + fill: transparent; } .summary .fromShare { margin-bottom: 3em; diff --git a/front/src/css/icons.css b/front/src/css/icons.css deleted file mode 100644 index 778eca8..0000000 --- a/front/src/css/icons.css +++ /dev/null @@ -1,67 +0,0 @@ -@font-face { - font-family: "icons"; - src: url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"); - font-weight: normal; - font-style: normal; -} -[class^="icon-"]:before, -[class*=" icon-"]:before { - font-family: "icons"; - display: inline-block; - vertical-align: middle; - line-height: 1; - font-weight: normal; - font-style: normal; - speak: none; - text-decoration: inherit; - text-transform: none; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.icon-arrow-left3:before, -.icon-bars:before, -.icon-lab:before, -.icon-list:before, -.icon-loop:before, -.icon-mobile:before, -.icon-question:before, -.icon-screen:before, -.icon-tablet:before, -.icon-warning:before { - font-family: "icons"; - display: inline-block; - font-weight: normal; - font-style: normal; - text-decoration: inherit; -} -.icon-arrow-left3:before { - content: "\f101"; -} -.icon-bars:before { - content: "\f102"; -} -.icon-lab:before { - content: "\f103"; -} -.icon-list:before { - content: "\f104"; -} -.icon-loop:before { - content: "\f105"; -} -.icon-mobile:before { - content: "\f106"; -} -.icon-question:before { - content: "\f107"; -} -.icon-screen:before { - content: "\f108"; -} -.icon-tablet:before { - content: "\f109"; -} -.icon-warning:before { - content: "\f10a"; -} diff --git a/front/src/css/index.css b/front/src/css/index.css index 20f1b18..2f5acde 100644 --- a/front/src/css/index.css +++ b/front/src/css/index.css @@ -55,17 +55,23 @@ text-decoration: none; font-size: 0.8em; } +.device .item > svg { + margin: 0.6em 0 0.3em; + fill: #fff; +} .device .item.active { color: #ffa319; border: 2px solid #ffa319; padding: 0; } +.device .item.active > svg { + fill: #ffa319; +} .device .item:hover { color: #ffa319; } -.device .item > div { - margin: 0.2em 0 0.1em; - font-size: 3em; +.device .item:hover > svg { + fill: #ffa319; } .settingsTooltip { position: relative; diff --git a/front/src/css/main.css b/front/src/css/main.css index add9c9c..2219d07 100644 --- a/front/src/css/main.css +++ b/front/src/css/main.css @@ -1,70 +1,3 @@ -@font-face { - font-family: "icons"; - src: url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"); - font-weight: normal; - font-style: normal; -} -[class^="icon-"]:before, -[class*=" icon-"]:before { - font-family: "icons"; - display: inline-block; - vertical-align: middle; - line-height: 1; - font-weight: normal; - font-style: normal; - speak: none; - text-decoration: inherit; - text-transform: none; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -.icon-arrow-left3:before, -.icon-bars:before, -.icon-lab:before, -.icon-list:before, -.icon-loop:before, -.icon-mobile:before, -.icon-question:before, -.icon-screen:before, -.icon-tablet:before, -.icon-warning:before { - font-family: "icons"; - display: inline-block; - font-weight: normal; - font-style: normal; - text-decoration: inherit; -} -.icon-arrow-left3:before { - content: "\f101"; -} -.icon-bars:before { - content: "\f102"; -} -.icon-lab:before { - content: "\f103"; -} -.icon-list:before { - content: "\f104"; -} -.icon-loop:before { - content: "\f105"; -} -.icon-mobile:before { - content: "\f106"; -} -.icon-question:before { - content: "\f107"; -} -.icon-screen:before { - content: "\f108"; -} -.icon-tablet:before { - content: "\f109"; -} -.icon-warning:before { - content: "\f10a"; -} html { margin: 100px 50px; } @@ -107,23 +40,41 @@ h1 span { cursor: pointer; text-decoration: none; } +.resultsMenu .menuItem svg { + fill: #fff; +} .resultsMenu .menuItem.back, .resultsMenu .menuItem.restart { color: #413; border-color: #413; } +.resultsMenu .menuItem.back svg, +.resultsMenu .menuItem.restart svg { + fill: #413; +} .resultsMenu .menuItem div { padding-top: 0.5em; font-size: 3em; } +.resultsMenu svg { + display: block; + margin: 1.2em auto 0.2em; +} .resultsMenu .active, .resultsMenu .menuItem.active:hover { color: #ffa319; border-color: #ffa319; } +.resultsMenu .active svg, +.resultsMenu .menuItem.active:hover svg { + fill: #ffa319; +} .resultsMenu .menuItem:hover { color: #ffa319; } +.resultsMenu .menuItem:hover svg { + fill: #ffa319; +} .resultsMenu span { position: relative; top: 0.5em; @@ -282,6 +233,26 @@ a.linkButton { width: 8.25em; height: 11em; } +.table { + display: table; + width: 100%; + border-spacing: 0.25em; +} +.table > div, +.table > a { + display: table-row; +} +.table > .headers > div { + font-weight: bold; + padding: 0.5em 1em; +} +.table > div > div, +.table > a > div { + padding: 0.1em 1em; + background: #f2f2f2; + display: table-cell; + text-align: left; +} .footer { padding: 3em; color: #413; @@ -298,7 +269,3 @@ a.linkButton { .footer .sponsor { font-size: 0.9em; } -[class^="icon-"]:before, -[class*=" icon-"]:before { - vertical-align: baseline; -} diff --git a/front/src/fonts/svg-icons/arrow-left3.svg b/front/src/fonts/svg-icons/arrow-left3.svg deleted file mode 100644 index edd6f1c..0000000 --- a/front/src/fonts/svg-icons/arrow-left3.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/src/fonts/svg-icons/bars.svg b/front/src/fonts/svg-icons/bars.svg deleted file mode 100644 index 03dac05..0000000 --- a/front/src/fonts/svg-icons/bars.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/src/fonts/svg-icons/lab.svg b/front/src/fonts/svg-icons/lab.svg deleted file mode 100644 index 0cf1263..0000000 --- a/front/src/fonts/svg-icons/lab.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/src/fonts/svg-icons/list.svg b/front/src/fonts/svg-icons/list.svg deleted file mode 100644 index 98a4cd3..0000000 --- a/front/src/fonts/svg-icons/list.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/src/fonts/svg-icons/loop.svg b/front/src/fonts/svg-icons/loop.svg deleted file mode 100755 index 84d3184..0000000 --- a/front/src/fonts/svg-icons/loop.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/src/fonts/svg-icons/mobile.svg b/front/src/fonts/svg-icons/mobile.svg deleted file mode 100755 index 5cb8089..0000000 --- a/front/src/fonts/svg-icons/mobile.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/src/fonts/svg-icons/question.svg b/front/src/fonts/svg-icons/question.svg deleted file mode 100644 index c78d38d..0000000 --- a/front/src/fonts/svg-icons/question.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/src/fonts/svg-icons/screen.svg b/front/src/fonts/svg-icons/screen.svg deleted file mode 100755 index c0cdff9..0000000 --- a/front/src/fonts/svg-icons/screen.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/src/fonts/svg-icons/tablet.svg b/front/src/fonts/svg-icons/tablet.svg deleted file mode 100755 index b701c87..0000000 --- a/front/src/fonts/svg-icons/tablet.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/src/fonts/svg-icons/warning.svg b/front/src/fonts/svg-icons/warning.svg deleted file mode 100644 index 3fd4284..0000000 --- a/front/src/fonts/svg-icons/warning.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/front/src/less/dashboard.less b/front/src/less/dashboard.less index 974ec04..79d2664 100644 --- a/front/src/less/dashboard.less +++ b/front/src/less/dashboard.less @@ -117,8 +117,8 @@ cursor: pointer; &.info { background: #FFF; - .icon-question { - color: #EBD8E2; + svg { + fill: #EBD8E2; } } } @@ -139,9 +139,13 @@ text-align: center; vertical-align: middle; } -.summary .notations .warning .label, .summary .notations .warning .result, .summary .notations .icon-warning { +.summary .notations .warning .label, .summary .notations .warning .result { color: #FF1919; } +.summary .notations .icon-warning svg { + fill: #FF1919; + margin: -2px 0; +} .summary .notations .criteria .info { width: 2%; text-align: center; @@ -150,8 +154,8 @@ padding-left: 0.1em; padding-right: 0.1em; } -.summary .notations .criteria .icon-question { - color: transparent; +.summary .notations .criteria .info svg { + fill: transparent; } .summary .fromShare { diff --git a/front/src/less/icons.less b/front/src/less/icons.less deleted file mode 100644 index 6d596a7..0000000 --- a/front/src/less/icons.less +++ /dev/null @@ -1,132 +0,0 @@ -// Generated by grunt-webfont -// Based on https://github.com/endtwist/fontcustom/blob/master/lib/fontcustom/templates/fontcustom.css - - -@font-face { - font-family:"icons"; - src:url("data:application/x-font-woff;charset=utf-8;base64,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") format("woff"); - font-weight:normal; - font-style:normal; -} - - -// Bootstrap Overrides -[class^="icon-"]:before, -[class*=" icon-"]:before { - font-family:"icons"; - display:inline-block; - vertical-align:middle; - line-height:1; - font-weight:normal; - font-style:normal; - speak:none; - text-decoration:inherit; - text-transform:none; - text-rendering:auto; - -webkit-font-smoothing:antialiased; - -moz-osx-font-smoothing:grayscale; -} - -// Mixins - -.icon-arrow-left3, -.icon-bars, -.icon-lab, -.icon-list, -.icon-loop, -.icon-mobile, -.icon-question, -.icon-screen, -.icon-tablet, -.icon-warning { - &:before { - font-family:"icons"; - display:inline-block; - font-weight:normal; - font-style:normal; - text-decoration:inherit; - } -} - - -// Icons - -.icon-arrow-left3 { - &:before { - content:"\f101"; - } - -} - - -.icon-bars { - &:before { - content:"\f102"; - } - -} - - -.icon-lab { - &:before { - content:"\f103"; - } - -} - - -.icon-list { - &:before { - content:"\f104"; - } - -} - - -.icon-loop { - &:before { - content:"\f105"; - } - -} - - -.icon-mobile { - &:before { - content:"\f106"; - } - -} - - -.icon-question { - &:before { - content:"\f107"; - } - -} - - -.icon-screen { - &:before { - content:"\f108"; - } - -} - - -.icon-tablet { - &:before { - content:"\f109"; - } - -} - - -.icon-warning { - &:before { - content:"\f10a"; - } - -} - diff --git a/front/src/less/index.less b/front/src/less/index.less index 6892452..88a01c8 100644 --- a/front/src/less/index.less +++ b/front/src/less/index.less @@ -59,19 +59,27 @@ text-decoration: none; font-size: 0.8em; + > svg { + margin: 0.6em 0 0.3em; + fill: #fff; + } + &.active { color: #ffa319; border: 2px solid #ffa319; padding: 0; + + > svg { + fill: #ffa319; + } } &:hover { color: #ffa319; - } - > div { - margin: 0.2em 0 0.1em; - font-size: 3em; + > svg { + fill: #ffa319; + } } } } diff --git a/front/src/less/main.less b/front/src/less/main.less index 3d5bbe3..05f9142 100644 --- a/front/src/less/main.less +++ b/front/src/less/main.less @@ -1,5 +1,3 @@ -@import "icons.less"; - html { margin: 100px 50px; } @@ -41,21 +39,42 @@ h1 span { border-radius: 0.5em; cursor: pointer; text-decoration: none; + + svg { + fill: #fff; + } + &.back, &.restart { color: #413; border-color: #413; + + svg { + fill: #413; + } } } .resultsMenu .menuItem div { padding-top: 0.5em; font-size: 3em; } +.resultsMenu svg { + display: block; + margin: 1.2em auto 0.2em; +} .resultsMenu .active, .resultsMenu .menuItem.active:hover { color: #ffa319; border-color: #ffa319; + + svg { + fill: #ffa319; + } } .resultsMenu .menuItem:hover { color: #ffa319; + + svg { + fill: #ffa319; + } } .resultsMenu span { position: relative; @@ -238,6 +257,27 @@ a.linkButton { } } +.table { + display: table; + width: 100%; + border-spacing: 0.25em; +} +.table > div, +.table > a { + display: table-row; +} +.table > .headers > div { + font-weight: bold; + padding: 0.5em 1em; +} +.table > div > div, +.table > a > div { + padding: 0.1em 1em; + background: #f2f2f2; + display: table-cell; + text-align: left; +} + .footer { padding: 3em; color: #413; @@ -253,8 +293,4 @@ a.linkButton { .sponsor { font-size: 0.9em; } -} - -[class^="icon-"]:before, [class*=" icon-"]:before { - vertical-align: baseline; } \ No newline at end of file diff --git a/front/src/main.html b/front/src/main.html index 231289c..0cebb8b 100644 --- a/front/src/main.html +++ b/front/src/main.html @@ -27,7 +27,7 @@ - +
diff --git a/front/src/views/index.html b/front/src/views/index.html index 0bed0eb..0eee70c 100644 --- a/front/src/views/index.html +++ b/front/src/views/index.html @@ -7,9 +7,9 @@
Choose the simulated device:
-
Desktop
-
Tablet
-
Phone
+ Desktop + Tablet + Phone
[ Advanced settings  ✚ diff --git a/front/src/views/resultSubHeader.html b/front/src/views/resultSubHeader.html index d31262a..b5b66a9 100644 --- a/front/src/views/resultSubHeader.html +++ b/front/src/views/resultSubHeader.html @@ -1,7 +1,7 @@
-
New test
-
Test again
- + New test + Test again +
\ No newline at end of file diff --git a/package.json b/package.json index b93711d..6275d91 100644 --- a/package.json +++ b/package.json @@ -90,7 +90,6 @@ "grunt-parallel": "~0.5.1", "grunt-replace": "~1.0.1", "grunt-usemin": "~3.1.1", - "grunt-webfont": "~1.7.2", "matchdep": "~2.0.0", "mocha": "~5.2.0", "sinon": "~7.2.3", From 17b5168bde6c40ff1aaadd6a20bf3e08795bb005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 9 Aug 2020 01:28:25 +0200 Subject: [PATCH 05/59] Node compatibility set to >=12.0 --- .travis.yml | 3 ++- lib/tools/redownload/gzipCompressor.js | 2 +- package.json | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index c14434c..a9a1f81 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,8 @@ language: node_js sudo: false node_js: - - "8.9" + - "12.18" + - "14.7" env: - CXX=g++-4.8 addons: diff --git a/lib/tools/redownload/gzipCompressor.js b/lib/tools/redownload/gzipCompressor.js index 82c9380..56351ab 100644 --- a/lib/tools/redownload/gzipCompressor.js +++ b/lib/tools/redownload/gzipCompressor.js @@ -58,7 +58,7 @@ var GzipCompressor = function() { var uncompressedSize = entry.weightCheck.optimized; - zlib.gzip(new Buffer(entry.weightCheck.bodyAfterOptimization, 'utf8'), function(err, buffer) { + zlib.gzip(Buffer.from(entry.weightCheck.bodyAfterOptimization, 'utf8'), function(err, buffer) { if (err) { debug('Could not compress minified file with gzip'); debug(err); diff --git a/package.json b/package.json index 6275d91..9162731 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "yellowlabtools": "./bin/cli.js" }, "engines": { - "node": ">= 8.0" + "node": ">= 12.0" }, "main": "./lib/index.js", "dependencies": { From caf3ddd68aea1ce76124baf048491ae5016330da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 9 Aug 2020 01:52:23 +0200 Subject: [PATCH 06/59] Font related stuff not needed anymore --- lib/tools/redownload/redownload.js | 9 --------- 1 file changed, 9 deletions(-) diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index 2b49753..0d4f5de 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -110,13 +110,6 @@ var Redownload = function() { var metrics = {}; var offenders = {}; - // Remove unused fonts that a normal browser would not download (fix #224) - results = results.filter(function(result) { - if (result && result.fontMetrics) { - return result.fontMetrics.isUsed !== false; - } - return true; - }); // Count requests offenders.totalRequests = listRequestsByType(results); @@ -141,13 +134,11 @@ var Redownload = function() { offenders.emptyRequests = listEmptyRequests(results); metrics.emptyRequests = offenders.emptyRequests.length; - // Now remove unwanted responses (redirections and empty files) results = results.filter(function(result) { return ((result.status < 300 || result.status >= 400) && result.weightCheck.bodySize > 0); }); - // Image compression offenders.imageOptimization = listImagesNotOptimized(results); metrics.imageOptimization = offenders.imageOptimization.totalGain; From 1d3dee19507d21125494a6e3985f31e952fa6be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 9 Aug 2020 02:06:20 +0200 Subject: [PATCH 07/59] Fix image optimization --- lib/tools/redownload/redownload.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index 0d4f5de..72b3957 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -136,7 +136,7 @@ var Redownload = function() { // Now remove unwanted responses (redirections and empty files) results = results.filter(function(result) { - return ((result.status < 300 || result.status >= 400) && result.weightCheck.bodySize > 0); + return (/* (result.status < 300 || result.status >= 400) && */ result.weightCheck.bodySize > 0); }); // Image compression From f49411b3a98181a62a7fe9e59daae5500cde00af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 10 Aug 2020 01:03:42 +0200 Subject: [PATCH 08/59] Various adaptations for Phantomas v2 --- front/src/js/controllers/ruleCtrl.js | 62 ------------- .../src/js/directives/offendersDirectives.js | 6 ++ front/src/views/rule.html | 26 ++++-- lib/metadata/policies.js | 86 ++----------------- lib/server/controllers/apiController.js | 3 +- lib/tools/colorDiff.js | 2 +- lib/tools/phantomas/phantomasWrapper.js | 4 + lib/tools/redownload/redownload.js | 2 +- 8 files changed, 40 insertions(+), 151 deletions(-) diff --git a/front/src/js/controllers/ruleCtrl.js b/front/src/js/controllers/ruleCtrl.js index 16c5c55..b2502c2 100644 --- a/front/src/js/controllers/ruleCtrl.js +++ b/front/src/js/controllers/ruleCtrl.js @@ -64,68 +64,6 @@ ruleCtrl.controller('RuleCtrl', ['$scope', '$rootScope', '$routeParams', '$locat } }; } - - // Init "Breakpoints" chart - if ($scope.policyName === 'cssBreakpoints' && $scope.rule.value > 0) { - - // Seek for the biggest breakpoint - var max = 0; - $scope.rule.offendersObj.forEach(function(offender) { - if (offender.pixels > max) { - max = offender.pixels; - } - }); - max = Math.max(max + 100, 1400); - - // We group offenders 10px by 10px - var GROUP_SIZE = 20; - - // Generate an empty array of values - $scope.breakpointsLabels = []; - $scope.breakpointsData = [[]]; - for (var i = 0; i <= max / GROUP_SIZE; i++) { - $scope.breakpointsLabels[i] = ''; - $scope.breakpointsData[0][i] = 0; - } - - // Fill it with results - $scope.rule.offendersObj.forEach(function(offender) { - var group = Math.floor((offender.pixels + 1) / GROUP_SIZE); - - if ($scope.breakpointsLabels[group] !== '') { - $scope.breakpointsLabels[group] += '/'; - } - $scope.breakpointsLabels[group] += offender.breakpoint; - - $scope.breakpointsData[0][group] += offender.count; - }); - - $scope.breakpointsSeries = ['Number of CSS rules per breakpoint']; - $scope.breakpointsColours = ['#9c4274']; - $scope.breakpointsOptions = { - scales: { - xAxes: [{ - gridLines: { - display:false - } - }], - yAxes: [{ - gridLines: { - display:false - } - }] - }, - tooltips: { - enabled: false - }, - - elements: { - point: { - radius: 0 - } - } - }; - } } $scope.backToDashboard = function() { diff --git a/front/src/js/directives/offendersDirectives.js b/front/src/js/directives/offendersDirectives.js index 9375973..c2663bc 100644 --- a/front/src/js/directives/offendersDirectives.js +++ b/front/src/js/directives/offendersDirectives.js @@ -291,4 +291,10 @@ }; }); + offendersDirectives.filter('roundNbr', function() { + return function(nbr) { + return Math.round(nbr); + }; + }); + })(); diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 105da75..2577dbe 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -39,6 +39,11 @@
+
+ {{offender.url}} + an iframe without URL +
+
{{offender.id}}: {{offender.occurrences}} occurrences
@@ -97,9 +102,9 @@
- inline CSS - - : + inline CSS + + :
@@ -132,7 +137,12 @@
-
+
+ + (offset: {{offender.offset | roundNbr}}px) +
+ +
@@ -148,7 +158,7 @@
- + cached for {{offender.ttlWithUnit}} {{offender.unit}}
@@ -345,7 +355,7 @@
-
+

Try to inline these styles in the head of the HTML or to merge them with other files.

Try to inline these scripts in the HTML or merge them with other files.

@@ -366,9 +376,7 @@
-
-

Breakpoints distribution graph

- +

Breakpoints list

diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 17ca8ac..aca3af8 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -35,7 +35,7 @@ var policies = { "isOkThreshold": 3, "isBadThreshold": 15, "isAbnormalThreshold": 30, - "hasOffenders": false + "hasOffenders": true }, "DOMidDuplicated": { "tool": "phantomas", @@ -378,45 +378,12 @@ var policies = { "isOkThreshold": 1000, "isBadThreshold": 3000, "isAbnormalThreshold": 4500, - "hasOffenders": true, - "offendersTransformFn": function(offenders) { - var hasInline = false; - var inlineCount = 0; - var files = []; - - offenders.forEach(function(line) { - if (line.indexOf('[inline CSS]: ') === 0) { - hasInline = true; - inlineCount += parseInt(line.substr(14)); - } else { - var parts = /^<(.*)>: (\d+)$/.exec(line); - - if (parts) { - files.push({ - file: parts[1], - rules: parseInt(parts[2], 10) - }); - } - } - }); - - if (hasInline) { - files.push({ - file: 'inline CSS', - rules: inlineCount - }); - } - - return { - count: files.length, - list: files - }; - } + "hasOffenders": true }, "cssComplexSelectors": { "tool": "phantomas", "label": "Complex selectors", - "message": "

Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".

They are adding more work for the browser, and this could be avoided by simplifying selectors. The B.E.M. methodology is an useful way to simplify your CSS.

", + "message": "

Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".

They are adding more work for the browser, and this could be avoided by simplifying selectors. The B.E.M. methodology is a useful way to simplify your CSS.

", "isOkThreshold": 0, "isBadThreshold": 800, "isAbnormalThreshold": 2000, @@ -442,10 +409,10 @@ var policies = { var deduplicatedObj = {}; offenders.map(function(offender) { - var parts = /^([^ ]*) \((\d+) times\)$/.exec(offender); + var parts = /^([^ ]*) \((\d+) times\)$/.exec(offender.value.message); if (!parts) { - debug('cssColors offenders transform function error with "%s"', offender); + debug('cssColors offenders transform function error with "%s"', offender.value.message); return; } @@ -553,7 +520,7 @@ var policies = { "hasOffenders": true, "offendersTransformFn": function(offenders) { var parsedOffenders = offenders.map(function(offender) { - var parts = /^(.*) \((\d+) times\) ?<(.*)>$/.exec(offender); + var parts = /^(.*) \((\d+) times\)$/.exec(offender.value.message); if (!parts) { debug('cssDuplicatedSelectors offenders transform function error with "%s"', offender); @@ -565,7 +532,7 @@ var policies = { return { rule: parts[1], occurrences: parseInt(parts[2], 10), - file: parts[3] + file: offender.value.url }; }); @@ -911,27 +878,7 @@ var policies = { "isOkThreshold": 10, "isBadThreshold": 25, "isAbnormalThreshold": 50, - "hasOffenders": true, - "offendersTransformFn": function(offenders) { - return { - count: offenders.length, - list: offenders.map(function(offender) { - var parts = /^([^ ]*): (\d+) request\(s\)$/.exec(offender); - - if (!parts) { - debug('domains offenders transform function error with "%s"', offender); - return { - file: offender - }; - } - - return { - domain: parts[1], - requests: parseInt(parts[2]) - }; - }) - }; - } + "hasOffenders": true }, "notFound": { "tool": "phantomas", @@ -1095,22 +1042,7 @@ var policies = { return { count: offenders.length, list: offenders - .map(function(offender) { - var parts = /^([^ ]*) cached for (-?\d+(\.\d+)?) s$/.exec(offender); - - if (!parts) { - debug('cachingTooShort offenders transform function error with "%s"', offender); - return { - file: offender - }; - } - - return { - file: parts[1], - ttl: Math.round(parseFloat(parts[2])) - }; - - }).sort(function(a, b) { + .sort(function(a, b) { return a.ttl - b.ttl; diff --git a/lib/server/controllers/apiController.js b/lib/server/controllers/apiController.js index fab7781..4c7c245 100644 --- a/lib/server/controllers/apiController.js +++ b/lib/server/controllers/apiController.js @@ -142,7 +142,8 @@ var ApiController = function(app) { delete data.params.options.screenshot; // Here we can remove tools results if not needed - + delete data.toolsResults.phantomas.offenders.requests; + return resultsDatastore.saveResult(data); }) diff --git a/lib/tools/colorDiff.js b/lib/tools/colorDiff.js index d7217d6..e1a436e 100644 --- a/lib/tools/colorDiff.js +++ b/lib/tools/colorDiff.js @@ -55,7 +55,7 @@ var colorDiff = function() { }; this.parseOffender = function(offender) { - var regexResult = /^(.*) \(\d+ times\)$/.exec(offender); + var regexResult = /^(.*) \(\d+ times\)$/.exec(offender.value.message); return regexResult ? regexResult[1] : null; }; diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index 8bd934f..ab1b36c 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -103,6 +103,10 @@ var PhantomasWrapper = function() { offenders: results.getAllOffenders() }; + // For debugging purpose only + var fs = require('fs'); + fs.writeFile('phantomas-response.json', JSON.stringify(json, null, 2), 'utf8', function(){}); + deferred.resolve(json); }). catch(res => { diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index 72b3957..a32837d 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -707,7 +707,7 @@ var Redownload = function() { // Always add gzip and webp headers before sending, in case the server listens to them var reqHeaders = []; reqHeaders['Accept'] = '*/*,image/webp'; - reqHeaders['Accept-Encoding'] = 'gzip, deflate, br'; + reqHeaders['Accept-Encoding'] = 'gzip, deflate'; reqHeaders['Connection'] = 'keep-alive'; var requestOptions = { From 6e6285c5ae46ede155052ab7557889befb23eac6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 10 Aug 2020 13:56:37 +0200 Subject: [PATCH 09/59] Various adaptations for Phantomas v2 --- front/src/views/rule.html | 8 ++-- lib/metadata/policies.js | 53 +++++---------------------- lib/metadata/scoreProfileGeneric.json | 3 +- 3 files changed, 14 insertions(+), 50 deletions(-) diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 2577dbe..58a3c30 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -45,7 +45,7 @@
- {{offender.id}}: {{offender.occurrences}} occurrences + {{offender.id}}: {{offender.count}} occurrences
@@ -123,7 +123,7 @@
- {{offender.property}} {{offender.message}} + {{offender.property}} {{offender.message}}
show hide @@ -183,8 +183,8 @@

in - - inline CSS + + inline CSS

diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index aca3af8..1239547 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -44,27 +44,7 @@ var policies = { "isOkThreshold": 0, "isBadThreshold": 5, "isAbnormalThreshold": 50, - "hasOffenders": true, - "offendersTransformFn": function(offenders) { - return { - count: offenders.length, - list: offenders.map(function(offender) { - var parts = /^(.*): ?(\d+) ?occurrences$/.exec(offender); - - if (!parts) { - debug('DOMidDuplicated offenders transform function error with "%s"', offender); - return { - parseError: offender - }; - } - - return { - id: parts[1], - occurrences: parseInt(parts[2], 10) - }; - }) - }; - } + "hasOffenders": true }, "DOMqueriesAvoidable": { "tool": "phantomas", @@ -330,7 +310,7 @@ var policies = { return { count: offenders.length, list: offenders.map(function(offender) { - if (offender === '[inline CSS] (Empty CSS was provided)') { + if (offender === '[inline CSS]') { return { error: 'Empty style tag', file: null, @@ -339,30 +319,15 @@ var policies = { }; } - var parts = /^(?:(?:<([^ \(]*)>|\[inline CSS\]) ?)?(?:\((((?! @ ).)*)(?: @ (\d+):(\d+))?\))?$/.exec(offender); - - if (parts) { + if (offender.value) { return { - error: parts[2] || 'Unknown parsing error' + (parts[1] ? '. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.' : ''), - file: parts[1] || null, - line: (parts[4] && parts[5]) ? parseInt(parts[4], 10) : null, - column: (parts[4] && parts[5]) ? parseInt(parts[5], 10) : null + error: offender.value.message || 'Unknown parsing error' + (parts[1] ? '. The entire file was ignored. As a result, the other CSS metrics and scores are miscalculated.' : ''), + file: offender.file || null, + line: offender.value.position ? offender.value.position.start.line : null, + column: offender.value.position ? offender.value.position.start.column : null }; } - // Try another syntax - parts = /^(.*) <(.*)> @ (\d+):(\d+)$/.exec(offender); - - if (parts) { - return { - error: parts[1] || 'Unknown parsing error', - file: parts[2] || null, - line: parseInt(parts[3], 10), - column: parseInt(parts[4], 10) - }; - } - - debug('cssParsingErrors offenders transform function error with "%s"', offender); return { parseError: offender @@ -971,7 +936,7 @@ var policies = { return offenders; } }, - "unusedUnicodeRanges": { + /*"unusedUnicodeRanges": { "tool": "redownload", "label": "Unused Unicode ranges", "message": "

This metric counts the number of unused Unicode ranges inside each font. For example, one font could include Cyrillic glyphs but none of them are used on the page.

It also reveals the number of ligatures (letters that are represented differently when close to each other) and hidden chars (glyphs not linked to the unicode system that can't be displayed on the web).

Because of technical limitations, Yellow Lab Tools checks each font against the glyphs of the entire page. As a result, estimated use is >= to reality. For example, if you read that 10 glyphs are \"possibly used\", it means that these 10 glyphs are used on the page but nothing guaranties that they are displayed using this font.

Tools such as Font Squirrel can remove some unicode ranges from a font.

In the case of an icon font, make sure you only keep the icons that are used on the website and to remove the others. Several tools are able to extract SVG images from a font, then some other tools can generate a font from the SVGs you want to keep.

", @@ -982,7 +947,7 @@ var policies = { "offendersTransformFn": function(offenders) { return offenders; } - }, + },*/ "http2": { "label": "HTTP/2 or SPDY", "message": "

HTTP/2 is the latest version of the HTTP protocol and is designed to optimize load speed. SPDY is deprecated but still very well supported.

The latest versions of all major browsers are now compatible. The difficulty is on the server side, where technologies are not quite ready yet.

", diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index b4f75e7..5646b26 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -82,8 +82,7 @@ "label": "Web fonts", "policies": { "fontsCount": 1, - "heavyFonts": 1, - "unusedUnicodeRanges": 1 + "heavyFonts": 1 } }, "serverConfig": { From 1bdfc8a5d23d71e53cba1914f62b998634a9449f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 11 Aug 2020 01:37:32 +0200 Subject: [PATCH 10/59] Rename blacklist and whitelist into block list and allowlist --- bin/cli.js | 4 ++-- front/src/js/services/apiService.js | 4 ++-- front/src/views/index.html | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index cacb0f8..e6c23bb 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -20,8 +20,8 @@ var cli = meow({ ' --cookie Adds a cookie on the main domain.', ' --auth-user Basic HTTP authentication username.', ' --auth-pass Basic HTTP authentication password.', - ' --block-domain Disallow requests to given (comma-separated) domains - aka blacklist.', - ' --allow-domain Only allow requests to given (comma-separated) domains - aka whitelist.', + ' --block-domain Disallow requests to given (comma-separated) domains.', + ' --allow-domain Only allow requests to given (comma-separated) domains.', ' --no-externals Block all domains except the main one.', ' --reporter The output format: "json" or "xml". Default is "json".', '' diff --git a/front/src/js/services/apiService.js b/front/src/js/services/apiService.js index 9162eca..01c6409 100644 --- a/front/src/js/services/apiService.js +++ b/front/src/js/services/apiService.js @@ -21,9 +21,9 @@ apiService.factory('API', ['$location', 'Runs', 'Results', function($location, R }; - if (settings.domainsBlackOrWhite === 'black') { + if (settings.domainsBlockOrAllow === 'block') { runObject.blockDomain = this.parseDomains(settings.domains); - } else if (settings.domainsBlackOrWhite === 'white') { + } else if (settings.domainsBlockOrAllow === 'allow') { var allowedDomains = this.parseDomains(settings.domains); if (allowedDomains.length > 0) { runObject.allowDomain = allowedDomains; diff --git a/front/src/views/index.html b/front/src/views/index.html index 0eee70c..2bd2544 100644 --- a/front/src/views/index.html +++ b/front/src/views/index.html @@ -83,8 +83,8 @@
- blacklist - whitelist + block list + allow list
From bafea756eec6542467eb74d821cc683023316cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 11 Aug 2020 02:04:59 +0200 Subject: [PATCH 11/59] Add brotli decompression to redownload.js --- lib/tools/redownload/redownload.js | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index a32837d..998021a 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -704,10 +704,10 @@ var Redownload = function() { debug('Downloading %s', entry.url); - // Always add gzip and webp headers before sending, in case the server listens to them + // Always add compression and webp headers before sending, in case the server listens to them var reqHeaders = []; reqHeaders['Accept'] = '*/*,image/webp'; - reqHeaders['Accept-Encoding'] = 'gzip, deflate'; + reqHeaders['Accept-Encoding'] = 'gzip, deflate, br'; reqHeaders['Connection'] = 'keep-alive'; var requestOptions = { @@ -836,6 +836,28 @@ var Redownload = function() { bodySize += data.length; }).pipe(deflate); + break; + case 'br': + + var brotli = zlib.createBrotliDecompress(); + + brotli.on('data', function (data) { + + bodyChunks.push(data); + uncompressedSize += data.length; + }).on('end', function () { + isCompressed = true; + tally(); + }).on('error', function(err) { + debug('Error while decoding %s', requestOptions.url); + debug(err); + callback(err); + }); + + res.on('data', function (data) { + bodySize += data.length; + }).pipe(brotli); + break; default: res.on('data', function (data) { From e19a521c4ecd425f8e44944df385223db1c28360 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 11 Aug 2020 03:01:43 +0200 Subject: [PATCH 12/59] Various adaptations for Phantomas v2 --- lib/metadata/policies.js | 18 ++++++----- lib/tools/phantomas/phantomasWrapper.js | 25 ++------------- test/core/colorDiffTest.js | 22 ++++++------- test/core/customPoliciesTest.js | 42 ++++++++++++++++++++++--- 4 files changed, 60 insertions(+), 47 deletions(-) diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 1239547..0614ec6 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -312,7 +312,7 @@ var policies = { list: offenders.map(function(offender) { if (offender === '[inline CSS]') { return { - error: 'Empty style tag', + error: 'Empty


and sponsored by Dareboost, the website speed test service
", "dashboard": null, + "dashboard": "
Test your website for free and get additional insights with our sponsor: dareboost.com
", + "about": "


This public instance of Yellow Lab Tools is generously hosted by



Yellow Lab Tools is also made possible thanks to our sponsor
" + } +} \ No newline at end of file diff --git a/server_config/settings-prod.json b/server_config/settings-prod.json index aff5cc0..272df56 100644 --- a/server_config/settings-prod.json +++ b/server_config/settings-prod.json @@ -2,7 +2,11 @@ "serverPort": 80, "phantomasEngine": "webkit", "googleAnalyticsId": "", - "screenshotWidth": 400, + "screenshotWidth": { + "desktop": 600, + "tablet": 420, + "phone": 360 + }, "baseUrl": "/", "authorizedKeys": { @@ -12,8 +16,8 @@ "blockedUrls": [], "sponsoring" : { - "home": "(this is a private instance)", - "dashboard": null, - "about": "(this is a private instance)" + "home": "Generously hosted by



and sponsored by Dareboost, the website speed test service
", "dashboard": null, + "dashboard": "
Test your website for free and get additional insights with our sponsor: dareboost.com
", + "about": "


This public instance of Yellow Lab Tools is generously hosted by



Yellow Lab Tools is also made possible thanks to our sponsor
" } } \ No newline at end of file diff --git a/server_config/settings.json b/server_config/settings.json index a23c7ff..5eabca0 100644 --- a/server_config/settings.json +++ b/server_config/settings.json @@ -2,7 +2,11 @@ "serverPort": 8383, "phantomasEngine": "webkit", "googleAnalyticsId": "", - "screenshotWidth": 400, + "screenshotWidth": { + "desktop": 600, + "tablet": 420, + "phone": 360 + }, "baseUrl": "/", "authorizedKeys": { diff --git a/test/api/screenshotHandlerTest.js b/test/api/screenshotHandlerTest.js index 267a44e..82a77bd 100644 --- a/test/api/screenshotHandlerTest.js +++ b/test/api/screenshotHandlerTest.js @@ -3,6 +3,7 @@ var ScreenshotHandler = require('../../lib/screenshotHandler'); var fs = require('fs'); var path = require('path'); +var rimraf = require('rimraf'); describe('screenshotHandler', function() { @@ -55,10 +56,14 @@ describe('screenshotHandler', function() { }); - it('should optimize an image and return a buffered version', function(done) { - ScreenshotHandler.optimize(imagePath, 200) + it('should create the tmp folder if it doesn\'t exist', function(done) { + // Delete tmp folder if it exists + rimraf.sync("/some/directory"); + + // The function we want to test + ScreenshotHandler.createTmpScreenshotFolder() .then(function(buffer) { - buffer.should.be.an.instanceof(Buffer); + fs.existsSync(path.join(__dirname, '../../tmp')).should.equal(true); done(); }) .fail(function(err) { @@ -66,63 +71,8 @@ describe('screenshotHandler', function() { }); }); - - it('should provide a temporary file object', function() { - screenshot = ScreenshotHandler.getScreenshotTempFile(); - - screenshot.should.have.a.property('getTmpFolder').that.is.a('function'); - screenshot.should.have.a.property('getTmpFilePath').that.is.a('function'); - screenshot.should.have.a.property('toThumbnail').that.is.a('function'); - screenshot.should.have.a.property('deleteTmpFile').that.is.a('function'); + it('should return the tmp folder path', function() { + ScreenshotHandler.getTmpFileRelativePath().should.equal('tmp/temp-screenshot.png'); }); - - it('should have created the temporary folder', function() { - var folder = screenshot.getTmpFolder(); - fs.existsSync(folder.path).should.equal(true); - }); - - - it('should respond a temporary file', function() { - var file = screenshot.getTmpFilePath(); - file.should.have.string('/screenshot.png'); - }); - - - it('should delete the temp folder when there is no file', function(done) { - var tmpFolderPath = screenshot; - - screenshot.deleteTmpFile() - .delay(1000) - .then(function() { - fs.existsSync(screenshot.getTmpFolder().path).should.equal(false); - done(); - }) - .fail(function(err) { - done(err); - }); - }); - - it('should delete the temp folder with the screenshot inside', function(done) { - screenshot = ScreenshotHandler.getScreenshotTempFile(); - var tmpFolderPath = screenshot.getTmpFolder().path; - var tmpImagePath = path.join(tmpFolderPath, 'screenshot.png'); - - // Copy image - var testImage = fs.readFileSync(imagePath); - fs.writeFileSync(tmpImagePath, testImage); - - fs.existsSync(tmpImagePath).should.equal(true); - - screenshot.deleteTmpFile() - .delay(1000) - .then(function() { - fs.existsSync(tmpImagePath).should.equal(false); - fs.existsSync(tmpFolderPath).should.equal(false); - done(); - }) - .fail(function(err) { - done(err); - }); - }); }); From a4f8e20293a277a56748e5811dc6506486351a50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 4 Oct 2020 02:44:16 +0200 Subject: [PATCH 14/59] Fix screenshots and continue implementing phantomas v2 --- lib/tools/phantomas/phantomasWrapper.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index 51b899d..917cf53 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -124,10 +124,6 @@ var PhantomasWrapper = function() { offenders: results.getAllOffenders() }; - // For debugging purpose only - var fs = require('fs'); - fs.writeFile('phantomas-response.json', JSON.stringify(json, null, 2), 'utf8', function(){}); - deferred.resolve(json); }). catch(res => { From 2bf3d5372bdb1bf9bc56758e58697276c3eef6e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 4 Oct 2020 02:54:00 +0200 Subject: [PATCH 15/59] Fix screenshots and continue implementing phantomas v2 --- lib/server/controllers/apiController.js | 2 +- lib/server/datastores/resultsDatastore.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/server/controllers/apiController.js b/lib/server/controllers/apiController.js index 84d1b88..fc41f0a 100644 --- a/lib/server/controllers/apiController.js +++ b/lib/server/controllers/apiController.js @@ -114,7 +114,7 @@ var ApiController = function(app) { if (screenshotBuffer) { debug('Image optimized'); data.screenshotBuffer = screenshotBuffer; - data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg' + data.screenshotUrl = '/api/results/' + data.runId + '/screenshot.jpg'; } }) diff --git a/lib/server/datastores/resultsDatastore.js b/lib/server/datastores/resultsDatastore.js index 262f53b..73336b8 100644 --- a/lib/server/datastores/resultsDatastore.js +++ b/lib/server/datastores/resultsDatastore.js @@ -17,7 +17,7 @@ function ResultsDatastore() { this.saveResult = function(testResults) { var screenshotFilePath = path.join(resultsDir, testResults.runId, resultScreenshotName); - var screenshotAPIPath = '/' + var screenshotAPIPath = '/'; return createResultFolder(testResults.runId) From 2d2da6e939e60f43bebd0e3b8c438a7361270a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 4 Oct 2020 02:57:53 +0200 Subject: [PATCH 16/59] Fix icons --- front/src/css/index.css | 3 +-- front/src/less/index.less | 3 +-- front/src/views/index.html | 12 ++++++------ 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/front/src/css/index.css b/front/src/css/index.css index 2f5acde..8259f44 100644 --- a/front/src/css/index.css +++ b/front/src/css/index.css @@ -76,8 +76,7 @@ .settingsTooltip { position: relative; } -.settingsTooltip span { - font-size: 0.8em; +.settingsTooltip svg { vertical-align: text-top; } .settingsTooltip div { diff --git a/front/src/less/index.less b/front/src/less/index.less index 88a01c8..64b991e 100644 --- a/front/src/less/index.less +++ b/front/src/less/index.less @@ -86,8 +86,7 @@ .settingsTooltip { position: relative; - span { - font-size: 0.8em; + svg { vertical-align: text-top; } diff --git a/front/src/views/index.html b/front/src/views/index.html index 2bd2544..a15205b 100644 --- a/front/src/views/index.html +++ b/front/src/views/index.html @@ -28,7 +28,7 @@
Wait selector - +
Wait for a CSS selector

Once the page is considered loaded, PhantomJS will repeatedly try to match the given CSS selector until it is found in the page. A 60 seconds timeout still applies anyway.

Example: "body.loaded"
@@ -38,7 +38,7 @@
Cookie - +
Cookie

Adds cookies, separated by a pipe character.

Example: "bar1=foo1;domain=.domain1.com|bar2=foo2;domain=www.domain2.com"
@@ -48,7 +48,7 @@
Authent - +
Basic HTTP authentication

Enter your credentials here if you need to bypass a basic authentication.

PS: if your authentication is not basic, you might be able to copy the session cookie from your browser, paste it in the "Cookie" setting and launch a run before your cookie expires.
@@ -67,7 +67,7 @@
HTTP proxy - +
HTTP proxy

Insert here your proxy settings with the format "host:port".

Example: "192.168.10.0:3333"
@@ -77,8 +77,8 @@
Block domains - -
Block some domains

One line per domain or subdomain.

Example:
google-analytics.com
ads.yahoo.com
ajax.googleapis.com


An empty whitelist will block all domains except the main domain.
+ +
Block some domains

One line per domain or subdomain.

Example:
google-analytics.com
ads.yahoo.com
ajax.googleapis.com


An empty allow list will block all domains except the main domain.
From f62e9baa4a03a77e9c64770d6e5cb91f3e844c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 4 Oct 2020 10:09:06 +0200 Subject: [PATCH 17/59] Add a new Desktop HP device (large DPR2 screen) --- bin/cli.js | 4 +- front/src/css/main.css | 12 ++++-- front/src/js/services/settingsService.js | 2 +- front/src/less/main.less | 2 +- front/src/views/dashboard.html | 2 +- front/src/views/index.html | 5 ++- front/src/views/screenshot.html | 2 +- lib/tools/phantomas/phantomasWrapper.js | 53 ++++-------------------- server_config/settings-prod.json | 5 ++- server_config/settings.json | 5 ++- 10 files changed, 32 insertions(+), 60 deletions(-) diff --git a/bin/cli.js b/bin/cli.js index e6c23bb..c03c922 100644 --- a/bin/cli.js +++ b/bin/cli.js @@ -13,7 +13,7 @@ var cli = meow({ ' yellowlabtools ', '', 'Options:', - ' --device Use "phone" or "tablet" to simulate a mobile device (by user-agent and viewport size).', + ' --device Simulates a device. Choose between phone (default), tablet, desktop and desktop-hd.', ' --screenshot Will take a screenshot and use this value as the output path. It needs to end with ".png".', //' --wait-for-selector Once the page is loaded, Phantomas will wait until the given CSS selector matches some elements.', ' --proxy Sets an HTTP proxy to pass through. Syntax is "host:port".', @@ -55,7 +55,7 @@ if (screenshot) { } // Device simulation -options.device = cli.flags.device || 'desktop'; +options.device = cli.flags.device || 'mobile'; // Wait for CSS selector options.waitForSelector = cli.flags.waitForSelector || null; diff --git a/front/src/css/main.css b/front/src/css/main.css index 2219d07..bb8866a 100644 --- a/front/src/css/main.css +++ b/front/src/css/main.css @@ -155,13 +155,15 @@ a.linkButton { .screenshotWrapper .screenshotError { color: #fff; } -.screenshotWrapper.desktop { +.screenshotWrapper.desktop, +.screenshotWrapper.desktop-hd { border: 0.2em solid #AAA; padding: 0.5em; border-top-left-radius: 0.4em; border-top-right-radius: 0.4em; } -.screenshotWrapper.desktop:before { +.screenshotWrapper.desktop:before, +.screenshotWrapper.desktop-hd:before { position: absolute; width: 15em; height: 0.6em; @@ -172,7 +174,8 @@ a.linkButton { border-bottom-right-radius: 0.2em; content: " "; } -.screenshotWrapper.desktop:after { +.screenshotWrapper.desktop:after, +.screenshotWrapper.desktop-hd:after { position: absolute; width: 0.4em; height: 0.2em; @@ -181,7 +184,8 @@ a.linkButton { background: lime; content: " "; } -.screenshotWrapper.desktop > div { +.screenshotWrapper.desktop > div, +.screenshotWrapper.desktop-hd > div { width: 12em; height: 6.75em; } diff --git a/front/src/js/services/settingsService.js b/front/src/js/services/settingsService.js index 81f9f5b..21674d4 100644 --- a/front/src/js/services/settingsService.js +++ b/front/src/js/services/settingsService.js @@ -6,7 +6,7 @@ settingsService.factory('Settings', ['localStorageService', function(localStorag getMergedSettings: function() { var defaultSettings = { - device: 'desktop', + device: 'mobile', showAdvanced: false }; diff --git a/front/src/less/main.less b/front/src/less/main.less index 05f9142..8413b02 100644 --- a/front/src/less/main.less +++ b/front/src/less/main.less @@ -168,7 +168,7 @@ a.linkButton { } } -.screenshotWrapper.desktop { +.screenshotWrapper.desktop, .screenshotWrapper.desktop-hd { border: 0.2em solid #AAA; padding: 0.5em; border-top-left-radius: 0.4em; diff --git a/front/src/views/dashboard.html b/front/src/views/dashboard.html index b07869c..cdce76d 100644 --- a/front/src/views/dashboard.html +++ b/front/src/views/dashboard.html @@ -18,7 +18,7 @@
-
+
Screenshot not available diff --git a/front/src/views/index.html b/front/src/views/index.html index a15205b..69d503a 100644 --- a/front/src/views/index.html +++ b/front/src/views/index.html @@ -7,9 +7,10 @@
[ Advanced settings  ✚ diff --git a/front/src/views/screenshot.html b/front/src/views/screenshot.html index 7f4de0d..c22bc42 100644 --- a/front/src/views/screenshot.html +++ b/front/src/views/screenshot.html @@ -2,7 +2,7 @@

Screenshot

-
+
Screenshot not available diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index 917cf53..8ffae1e 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -17,49 +17,14 @@ var PhantomasWrapper = function() { var deferred = Q.defer(); var task = data.params; - /*var options = { - - // Cusomizable options - 'engine': task.options.phantomasEngine || 'webkit', - 'timeout': task.options.timeout || 30, - 'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/27.0.1453.110 Safari/537.36' : null, - 'tablet': (task.options.device === 'tablet'), - 'phone': (task.options.device === 'phone'), - 'screenshot': task.options.screenshot || false, - 'wait-for-selector': task.options.waitForSelector, - 'cookie': task.options.cookie, - 'auth-user': task.options.authUser, - 'auth-pass': task.options.authPass, - 'block-domain': task.options.blockDomain, - 'allow-domain': task.options.allowDomain, - 'no-externals': task.options.noExternals, - - // Mandatory - 'reporter': 'json:pretty', - 'analyze-css': true, - 'ignore-ssl-errors': true, - 'skip-modules': [ - 'ajaxRequests', // overridden - 'domHiddenContent', // overridden - 'domMutations', // not compatible with webkit - 'domQueries', // overridden - 'events', // overridden - 'filmStrip', // not needed - 'har', // not needed for the moment - 'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT - 'jQuery', // overridden - 'jserrors', // overridden - 'lazyLoadableImages', //overridden - 'pageSource', // not needed - 'windowPerformance' // overridden - ].join(','), - 'include-dirs': [ - path.join(__dirname, 'custom_modules/core'), - path.join(__dirname, 'custom_modules/modules') - ].join(',') - }; - - */ + var viewportOption = null; + if (task.options.device === 'desktop') { + // Similar to an old non-retina Macbook Air 13" + viewportOption = '1280x800x1'; + } else if (task.options.device === 'desktop-hd') { + // Similar to a retina Macbook Pro 16" + viewportOption = '1536x960x2'; + } var options = { @@ -69,7 +34,7 @@ var PhantomasWrapper = function() { 'tablet': (task.options.device === 'tablet'), 'phone': (task.options.device === 'phone'), 'screenshot': task.options.screenshot || false, - 'viewport': (task.options.device === 'desktop') ? '1280x800x1' : null, + 'viewport': viewportOption, 'wait-for-network-idle': true, //'wait-for-selector': task.options.waitForSelector, 'cookie': task.options.cookie, diff --git a/server_config/settings-prod.json b/server_config/settings-prod.json index 272df56..a6efafd 100644 --- a/server_config/settings-prod.json +++ b/server_config/settings-prod.json @@ -3,9 +3,10 @@ "phantomasEngine": "webkit", "googleAnalyticsId": "", "screenshotWidth": { - "desktop": 600, + "phone": 360, "tablet": 420, - "phone": 360 + "desktop": 600, + "desktop-hd": 600 }, "baseUrl": "/", "authorizedKeys": { diff --git a/server_config/settings.json b/server_config/settings.json index 5eabca0..f8f5896 100644 --- a/server_config/settings.json +++ b/server_config/settings.json @@ -3,9 +3,10 @@ "phantomasEngine": "webkit", "googleAnalyticsId": "", "screenshotWidth": { - "desktop": 600, + "phone": 360, "tablet": 420, - "phone": 360 + "desktop": 600, + "desktop-hd": 600 }, "baseUrl": "/", "authorizedKeys": { From 4287f60ab3ccdbe01dc4be8f5f9c1c09dad80665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 4 Oct 2020 13:23:43 +0200 Subject: [PATCH 18/59] Adapt Oversized Images detection to our new screen resolutions --- lib/metadata/policies.js | 2 +- lib/tools/phantomas/phantomasWrapper.js | 23 +++-------------------- lib/tools/redownload/redownload.js | 14 ++++++++++---- 3 files changed, 14 insertions(+), 25 deletions(-) diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 0614ec6..06306ca 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -803,7 +803,7 @@ var policies = { "imagesTooLarge": { "tool": "redownload", "label": "Oversized images", - "message": "

This is the number of images with a width >800px on mobile or >1500px on desktop. Try reducing their size.

Please ignore if the file is used as a sprite.

Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect detection.

", + "message": "

This is the number of images with a width >1200px on mobile, >1800px on tablet, >2400 on desktop, >3200px on HD desktop. Try reducing their size.

Please ignore if the file is used as a sprite.

", "isOkThreshold": 0, "isBadThreshold": 5, "isAbnormalThreshold": 10, diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index 8ffae1e..308c0c5 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -18,6 +18,8 @@ var PhantomasWrapper = function() { var task = data.params; var viewportOption = null; + // Setting screen dimensions for desktop devices only. + // Phone and tablet dimensions are dealt by Phantomas. if (task.options.device === 'desktop') { // Similar to an old non-retina Macbook Air 13" viewportOption = '1280x800x1'; @@ -46,26 +48,7 @@ var PhantomasWrapper = function() { // Mandatory 'analyze-css': true, - 'ignore-ssl-errors': true, - /*'skip-modules': [ - 'ajaxRequests', // overridden - 'domHiddenContent', // overridden - 'domMutations', // not compatible with webkit - 'domQueries', // overridden - 'events', // overridden - 'filmStrip', // not needed - 'har', // not needed for the moment - 'javaScriptBottlenecks', // needs to be launched after custom module scopeYLT - 'jQuery', // overridden - 'jserrors', // overridden - 'lazyLoadableImages', //overridden - 'pageSource', // not needed - 'windowPerformance' // overridden - ].join(','),*/ - /*'include-dirs': [ - path.join(__dirname, 'custom_modules/core'), - path.join(__dirname, 'custom_modules/modules') - ].join(',')*/ + 'ignore-ssl-errors': true }; diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index 998021a..d770df7 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -144,8 +144,7 @@ var Redownload = function() { metrics.imageOptimization = offenders.imageOptimization.totalGain; // Image width - var isMobile = data.params.options.device === 'phone'; - offenders.imagesTooLarge = listImagesTooLarge(results, isMobile); + offenders.imagesTooLarge = listImagesTooLarge(results, data.params.options.device); metrics.imagesTooLarge = offenders.imagesTooLarge.length; // File minification @@ -359,13 +358,20 @@ var Redownload = function() { return results; } - function listImagesTooLarge(requests, isMobile) { + function listImagesTooLarge(requests, device) { var results = []; requests.forEach(function(req) { + const thresholds = { + 'phone': 1200, + 'tablet': 1800, + 'desktop': 2400, + 'desktop-hd': 3200 + }; + if (req.weightCheck.bodySize > 0 && req.imageDimensions && - ((isMobile && req.imageDimensions.width > 800) || req.imageDimensions.width > 1500)) { + req.imageDimensions.width > thresholds[device]) { results.push({ url: req.url, From 93b81493c07f8d0a7639e05c706c253adb01fa32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 4 Oct 2020 18:50:28 +0200 Subject: [PATCH 19/59] Graphical changes, essentially colors --- front/src/css/about.css | 5 ++++- front/src/css/dashboard.css | 4 ++-- front/src/css/index.css | 7 ++++--- front/src/css/main.css | 24 ++++++++++-------------- front/src/css/rule.css | 2 +- front/src/js/app.js | 2 +- front/src/less/about.less | 6 +++++- front/src/less/dashboard.less | 4 ++-- front/src/less/index.less | 7 ++++--- front/src/less/main.less | 25 +++++++++++-------------- front/src/less/rule.less | 2 +- front/src/main.html | 5 +---- front/src/views/about.html | 4 ++-- 13 files changed, 48 insertions(+), 49 deletions(-) diff --git a/front/src/css/about.css b/front/src/css/about.css index 0436d82..8bf7c0d 100644 --- a/front/src/css/about.css +++ b/front/src/css/about.css @@ -6,5 +6,8 @@ margin: 2em; } .about a { - color: #FFF; + color: #fff; +} +.sponsor { + color: #ffa319; } diff --git a/front/src/css/dashboard.css b/front/src/css/dashboard.css index f070c80..26246cf 100644 --- a/front/src/css/dashboard.css +++ b/front/src/css/dashboard.css @@ -114,14 +114,14 @@ color: inherit; } .summary .notations .criteria .table > a:hover > div { - background: #EBD8E2; + background: #d8ebe0; cursor: pointer; } .summary .notations .criteria .table > a:hover > div.info { background: #FFF; } .summary .notations .criteria .table > a:hover > div.info svg { - fill: #EBD8E2; + fill: #d8ebe0; } .summary .notations .criteria .grade { width: 10%; diff --git a/front/src/css/index.css b/front/src/css/index.css index 8259f44..54ad711 100644 --- a/front/src/css/index.css +++ b/front/src/css/index.css @@ -20,7 +20,7 @@ background: #e74c3c; } .launchBtn.disabled { - background: #deaca6; + background: #f1bd70; } .launchBtn.disabled:focus { color: #ddd; @@ -90,6 +90,8 @@ border-radius: 1em; border: 2px solid #ffa319; white-space: normal; + word-break: break-all; + word-break: break-word; z-index: 2; } .settingsTooltip:hover div { @@ -154,7 +156,7 @@ width: 50%; margin: 6em auto 0; font-size: 0.9em; - color: #413; + color: #8abfaf; } .features > div { width: 33.3%; @@ -169,7 +171,6 @@ input.url { height: 2em; border: 0 solid; border-radius: 0.5em; - box-shadow: 0.1em 0.2em 0 0 #5e2846; outline: none; } input[type=submit]:hover { diff --git a/front/src/css/main.css b/front/src/css/main.css index bb8866a..95fd3ba 100644 --- a/front/src/css/main.css +++ b/front/src/css/main.css @@ -4,7 +4,7 @@ html { body { margin: 0 auto; max-width: 1280px; - background: #9c4274; + background: #212240; color: #fff; font-size: 16px; text-align: center; @@ -20,11 +20,8 @@ button { input[type=submit] { cursor: pointer; } -h1 span { - display: inline-block; - height: 1em; - width: 1em; - color: #ffa319; +h1 { + font-weight: 200; } .resultsMenu { margin-top: 2em; @@ -35,7 +32,7 @@ h1 span { width: 8em; height: 7em; color: #fff; - border: 3px solid #fff; + border: 2px solid #fff; border-radius: 0.5em; cursor: pointer; text-decoration: none; @@ -45,12 +42,8 @@ h1 span { } .resultsMenu .menuItem.back, .resultsMenu .menuItem.restart { - color: #413; - border-color: #413; -} -.resultsMenu .menuItem.back svg, -.resultsMenu .menuItem.restart svg { - fill: #413; + color: #fff; + border-color: #fff; } .resultsMenu .menuItem div { padding-top: 0.5em; @@ -259,7 +252,7 @@ a.linkButton { } .footer { padding: 3em; - color: #413; + color: #fff; } .footer a { color: inherit; @@ -273,3 +266,6 @@ a.linkButton { .footer .sponsor { font-size: 0.9em; } +.homeSponsor { + color: #ffa319; +} diff --git a/front/src/css/rule.css b/front/src/css/rule.css index c097224..97e5bec 100644 --- a/front/src/css/rule.css +++ b/front/src/css/rule.css @@ -80,7 +80,7 @@ word-break: break-all; } .rule .offendersTable > div > div:hover { - background: #EBD8E2; + background: #d8ebe0; } .rule .notFound { font-size: 1em; diff --git a/front/src/js/app.js b/front/src/js/app.js index cdaf143..4b04f1e 100644 --- a/front/src/js/app.js +++ b/front/src/js/app.js @@ -41,7 +41,7 @@ yltApp.run(['$rootScope', '$location', function($rootScope, $location) { // GitHub star button (asynchronously loaded iframe) window.addEventListener('load', function() { - window.document.getElementById('ghbtn').src = 'https://ghbtns.com/github-btn.html?user=gmetais&repo=YellowLabTools&type=star&count=true&size=large'; + window.document.getElementById('ghbtn').src = 'https://ghbtns.com/github-btn.html?user=YellowLabTools&repo=YellowLabTools&type=star&count=true&size=large'; }); }]); diff --git a/front/src/less/about.less b/front/src/less/about.less index 67de419..3c20359 100644 --- a/front/src/less/about.less +++ b/front/src/less/about.less @@ -8,5 +8,9 @@ } .about a { - color: #FFF; + color: #fff; +} + +.sponsor { + color: #ffa319; } \ No newline at end of file diff --git a/front/src/less/dashboard.less b/front/src/less/dashboard.less index 79d2664..ce5fdd9 100644 --- a/front/src/less/dashboard.less +++ b/front/src/less/dashboard.less @@ -113,12 +113,12 @@ color: inherit; } > a:hover > div { - background: #EBD8E2; + background: #d8ebe0; cursor: pointer; &.info { background: #FFF; svg { - fill: #EBD8E2; + fill: #d8ebe0; } } } diff --git a/front/src/less/index.less b/front/src/less/index.less index 64b991e..e212fcd 100644 --- a/front/src/less/index.less +++ b/front/src/less/index.less @@ -22,7 +22,7 @@ background: #e74c3c; } &.disabled { - background: #deaca6; + background: #f1bd70; &:focus { color: #ddd; } @@ -101,6 +101,8 @@ border-radius: 1em; border: 2px solid #ffa319; white-space: normal; + word-break: break-all; + word-break: break-word; z-index: 2; } @@ -178,7 +180,7 @@ width: 50%; margin: 6em auto 0; font-size: 0.9em; - color: #413; + color: #8abfaf; > div { width: 33.3%; @@ -194,7 +196,6 @@ input[type=submit], input.url { height: 2em; border: 0 solid; border-radius: 0.5em; - box-shadow: 0.1em 0.2em 0 0 #5e2846; outline: none; } input[type=submit]:hover { diff --git a/front/src/less/main.less b/front/src/less/main.less index 8413b02..0ea6346 100644 --- a/front/src/less/main.less +++ b/front/src/less/main.less @@ -5,7 +5,7 @@ html { body { margin: 0 auto; max-width: 1280px; - background: #9c4274; + background: #212240; color: #fff; font-size: 16px; text-align: center; @@ -19,11 +19,8 @@ input[type=submit] { cursor: pointer; } -h1 span { - display: inline-block; - height: 1em; - width: 1em; - color: #ffa319; +h1 { + font-weight: 200; } .resultsMenu { @@ -35,7 +32,7 @@ h1 span { width: 8em; height: 7em; color: #fff; - border: 3px solid #fff; + border: 2px solid #fff; border-radius: 0.5em; cursor: pointer; text-decoration: none; @@ -45,12 +42,8 @@ h1 span { } &.back, &.restart { - color: #413; - border-color: #413; - - svg { - fill: #413; - } + color: #fff; + border-color: #fff; } } .resultsMenu .menuItem div { @@ -280,7 +273,7 @@ a.linkButton { .footer { padding: 3em; - color: #413; + color: #fff; a { color: inherit; } @@ -293,4 +286,8 @@ a.linkButton { .sponsor { font-size: 0.9em; } +} + +.homeSponsor { + color: #ffa319; } \ No newline at end of file diff --git a/front/src/less/rule.less b/front/src/less/rule.less index c139a34..b5c5827 100644 --- a/front/src/less/rule.less +++ b/front/src/less/rule.less @@ -85,7 +85,7 @@ word-wrap: break-word; word-break: break-all; &:hover { - background: #EBD8E2; + background: #d8ebe0; } } } diff --git a/front/src/main.html b/front/src/main.html index 0cebb8b..8d53753 100644 --- a/front/src/main.html +++ b/front/src/main.html @@ -18,11 +18,8 @@ - - - @@ -32,7 +29,7 @@
diff --git a/front/src/views/about.html b/front/src/views/about.html index e838117..d0c0e51 100644 --- a/front/src/views/about.html +++ b/front/src/views/about.html @@ -1,9 +1,9 @@
-

Yellow Lab Tools is an open source project by Gaël Métais. It allows you to test a webpage (via an URL) and detects performance and front-end code quality issues.

+

Yellow Lab Tools is an open source project by Gaël Métais. It allows you to test a webpage (via an URL) and detects performance and front-end code quality issues.

This is done by loading the webpage via PhantomJS and collecting various metrics and statistics with the help of Phantomas. These metrics are categorized and transformed into scores. It also gives in-depth details so that developers can correct the detected issues.

-

By the way, it's free because we are geeks, not businessmen. All we want is a on GitHub. It will boost our motivation to add more awesome features!!!

+

By the way, it's free because I'm a geek, not a businessman. In return, you can add a on GitHub. It will boost my motivation to add more awesome features!!!

<%if (sponsoring.about) { %> From e2eacbc0514dd7aeb05d333e56a079934bb49b8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 5 Oct 2020 12:49:26 +0200 Subject: [PATCH 20/59] Responsive design for mobile (finally) --- front/src/css/dashboard.css | 110 ++++++++++++++++++++++++--------- front/src/css/main.css | 22 +++++-- front/src/css/rule.css | 37 ++++++----- front/src/css/screenshot.css | 12 +++- front/src/less/dashboard.less | 96 +++++++++++++++++++--------- front/src/less/main.less | 19 ++++-- front/src/less/rule.less | 38 +++++++----- front/src/less/screenshot.less | 10 ++- front/src/main.html | 1 + lib/metadata/policies.js | 6 +- 10 files changed, 249 insertions(+), 102 deletions(-) diff --git a/front/src/css/dashboard.css b/front/src/css/dashboard.css index 26246cf..78313eb 100644 --- a/front/src/css/dashboard.css +++ b/front/src/css/dashboard.css @@ -5,15 +5,8 @@ text-align: center; } .summary .globalScore { - display: table; - width: 60%; margin: 3em auto; } -.summary .globalScore > div { - display: table-cell; - width: 50%; - vertical-align: middle; -} .summary .globalScore .globalGrade { margin: 0.5 auto; width: 2.5em; @@ -21,12 +14,10 @@ line-height: 2.5em; border-radius: 0.5em; font-size: 3em; - font-weight: bold; vertical-align: middle; } .summary .globalScore .on100 { font-size: 1.2em; - font-weight: bold; margin: 0.5em 0 1em; } .summary .globalScore .screenshotWrapper:hover { @@ -54,28 +45,63 @@ top: 1.5em; left: 0.9em; } +@media (min-width: 820px) { + .summary .globalScore { + width: 65%; + display: table; + } + .summary .globalScore > div { + display: table-cell; + width: 50%; + vertical-align: middle; + } +} .summary .notations { + width: 100%; display: table; - width: 80%; - margin: 0 10% 1.5em; - border-spacing: 1em; + margin: 0 0 1.5em; + border-spacing: 0 1em; +} +@media (min-width: 820px) { + .summary .notations { + width: 80%; + margin: 0 10% 1.5em; + border-spacing: 1em; + } } .summary .notations > div { display: table-row; } .summary .notations > div > div { - display: table-cell; - height: 2.5em; vertical-align: middle; } +@media (min-width: 820px) { + .summary .notations > div > div { + display: table-cell; + height: 2.5em; + } +} .summary .notations .category { font-weight: bold; - text-align: center; - width: 20%; + width: 50%; + float: left; + text-align: left; + margin: 1em 0.25em; +} +@media (min-width: 820px) { + .summary .notations .category { + width: 20%; + text-align: center; + float: none; + } } .summary .notations .criteria { font-weight: normal; - width: 75%; +} +@media (min-width: 820px) { + .summary .notations .criteria { + width: 75%; + } } .summary .notations .A.categoryScore, .summary .notations .B.categoryScore, @@ -87,10 +113,23 @@ width: 2.5em; max-width: 2.5em; min-width: 2.5em; - font-size: 2em; + margin: 0.2em; + font-size: 1.5em; text-align: center; border-radius: 0.5em; - font-weight: bold; + float: right; +} +@media (min-width: 820px) { + .summary .notations .A.categoryScore, + .summary .notations .B.categoryScore, + .summary .notations .C.categoryScore, + .summary .notations .D.categoryScore, + .summary .notations .E.categoryScore, + .summary .notations .F.categoryScore, + .summary .notations .NA.categoryScore { + float: none; + font-size: 2em; + } } .summary .notations .grade .A, .summary .notations .grade .B, @@ -134,7 +173,6 @@ } .summary .notations .criteria .result { width: 18%; - font-weight: bold; white-space: nowrap; text-align: center; vertical-align: middle; @@ -148,12 +186,18 @@ margin: -2px 0; } .summary .notations .criteria .info { - width: 2%; - text-align: center; - vertical-align: middle; - background: #FFF; - padding-left: 0.1em; - padding-right: 0.1em; + display: none; +} +@media (min-width: 820px) { + .summary .notations .criteria .info { + display: table-cell; + width: 2%; + text-align: center; + vertical-align: middle; + background: #FFF; + padding-left: 0.1em; + padding-right: 0.1em; + } } .summary .notations .criteria .info svg { fill: transparent; @@ -161,8 +205,16 @@ .summary .fromShare { margin-bottom: 3em; } +.summary .tweet { + display: none; +} +@media (min-width: 820px) { + .summary .tweet { + display: block; + } +} .summary .tweet .tweetText { - color: #413; + color: #212240; background: #F2F2F2; border: none; width: 25em; @@ -172,7 +224,7 @@ } .summary .tweet .tweetButton, .summary .tweet .linkedinButton { - color: #413; + color: #212240; background: #F2F2F2; margin-right: 0; } @@ -187,7 +239,7 @@ .summary .sponsor { font-size: 0.9em; margin-bottom: 4em; - color: #413; + color: #ffa319; } .summary .sponsor a { color: inherit; diff --git a/front/src/css/main.css b/front/src/css/main.css index 95fd3ba..d45ff6e 100644 --- a/front/src/css/main.css +++ b/front/src/css/main.css @@ -1,5 +1,10 @@ html { - margin: 100px 50px; + margin: 35px 5px; +} +@media (min-width: 640px) { + html { + margin: 100px 50px; + } } body { margin: 0 auto; @@ -27,16 +32,23 @@ h1 { margin-top: 2em; } .resultsMenu .menuItem { + font-size: 0.8em; display: inline-block; - margin: 1em; - width: 8em; + width: 7em; height: 7em; color: #fff; - border: 2px solid #fff; - border-radius: 0.5em; cursor: pointer; text-decoration: none; } +@media (min-width: 640px) { + .resultsMenu .menuItem { + font-size: 1em; + margin: 1em; + width: 8em; + border: 2px solid #fff; + border-radius: 0.5em; + } +} .resultsMenu .menuItem svg { fill: #fff; } diff --git a/front/src/css/rule.css b/front/src/css/rule.css index 97e5bec..24e789e 100644 --- a/front/src/css/rule.css +++ b/front/src/css/rule.css @@ -2,7 +2,6 @@ text-align: center; } .rule .ruleTable { - display: table; border-spacing: 1em; width: 90%; margin: 2em auto; @@ -10,16 +9,20 @@ border: 1px dashed #666; border-radius: 0.5em; } -.rule .ruleTable > div { - display: table-cell; - vertical-align: middle; -} -.rule .ruleTable .left { - width: 33%; - font-weight: bold; -} -.rule .ruleTable .right { - width: 67%; +@media (min-width: 820px) { + .rule .ruleTable { + display: table; + } + .rule .ruleTable > div { + display: table-cell; + vertical-align: middle; + } + .rule .ruleTable .left { + width: 33%; + } + .rule .ruleTable .right { + width: 67%; + } } .rule .score { font-size: 2.5em; @@ -27,7 +30,7 @@ height: 2em; width: 2em; border-radius: 0.5em; - margin: 0 auto 0.5em; + margin: 0 auto 0.25em; } .rule h3 { margin-bottom: 0em; @@ -67,7 +70,13 @@ border-spacing: 0 0.25em; margin: 0 auto; min-width: 10%; - max-width: 90%; + font-size: 0.875em; +} +@media (min-width: 820px) { + .rule .offendersTable { + max-width: 90%; + font-size: 1em; + } } .rule .offendersTable > div { display: table-row; @@ -75,7 +84,7 @@ .rule .offendersTable > div > div { display: table-cell; background: #f2f2f2; - padding: 0 1em; + padding: 0 0.25em; word-wrap: break-word; word-break: break-all; } diff --git a/front/src/css/screenshot.css b/front/src/css/screenshot.css index 505d893..0df36d4 100644 --- a/front/src/css/screenshot.css +++ b/front/src/css/screenshot.css @@ -2,6 +2,16 @@ text-align: center; } .screenshot .screenshotWrapper { - font-size: 2.08333333em; + font-size: 1.2em; margin-bottom: 0.5em; } +@media (min-width: 420px) { + .screenshot .screenshotWrapper { + font-size: 1.6em; + } +} +@media (min-width: 640px) { + .screenshot .screenshotWrapper { + font-size: 2.08333333em; + } +} diff --git a/front/src/less/dashboard.less b/front/src/less/dashboard.less index ce5fdd9..9f2ca51 100644 --- a/front/src/less/dashboard.less +++ b/front/src/less/dashboard.less @@ -7,16 +7,8 @@ } .summary .globalScore { - display: table; - width: 60%; margin: 3em auto; - > div { - display: table-cell; - width: 50%; - vertical-align: middle; - } - .globalGrade { margin: 0.5 auto; width: 2.5em; @@ -24,12 +16,10 @@ line-height: 2.5em; border-radius: 0.5em; font-size: 3em; - font-weight: bold; vertical-align: middle; } .on100 { font-size: 1.2em; - font-weight: bold; margin: 0.5em 0 1em; } @@ -61,40 +51,77 @@ top: 1.5em; left: 0.9em; } + + @media (min-width: 820px) { + width: 65%; + display: table; + + > div { + display: table-cell; + width: 50%; + vertical-align: middle; + } + } } .summary .notations { + width: 100%; display: table; - width: 80%; - margin: 0 10% 1.5em; - border-spacing: 1em; + margin: 0 0 1.5em; + border-spacing: 0 1em; + + @media (min-width: 820px) { + width: 80%; + margin: 0 10% 1.5em; + border-spacing: 1em; + } } .summary .notations > div { display: table-row; } .summary .notations > div > div { - display: table-cell; - height: 2.5em; vertical-align: middle; + + @media (min-width: 820px) { + display: table-cell; + height: 2.5em; + } } .summary .notations .category { font-weight: bold; - text-align: center; - width: 20%; + width: 50%; + float: left; + text-align: left; + margin: 1em 0.25em; + + @media (min-width: 820px) { + width: 20%; + text-align: center; + float: none; + } } .summary .notations .criteria { font-weight: normal; - width: 75%; + + @media (min-width: 820px) { + width: 75%; + } } .A, .B, .C, .D, .E, .F, .NA { .summary .notations &.categoryScore { width: 2.5em; max-width: 2.5em; min-width: 2.5em; - font-size: 2em; + margin: 0.2em; + font-size: 1.5em; text-align: center; border-radius: 0.5em; - font-weight: bold; + float: right; + + @media (min-width: 820px) { + float: none; + font-size: 2em; + } } .summary .notations .grade & { width: 1em; @@ -134,7 +161,6 @@ } .summary .notations .criteria .result { width: 18%; - font-weight: bold; white-space: nowrap; text-align: center; vertical-align: middle; @@ -147,12 +173,17 @@ margin: -2px 0; } .summary .notations .criteria .info { - width: 2%; - text-align: center; - vertical-align: middle; - background: #FFF; - padding-left: 0.1em; - padding-right: 0.1em; + display: none; + + @media (min-width: 820px) { + display: table-cell; + width: 2%; + text-align: center; + vertical-align: middle; + background: #FFF; + padding-left: 0.1em; + padding-right: 0.1em; + } } .summary .notations .criteria .info svg { fill: transparent; @@ -163,8 +194,13 @@ } .summary .tweet { + display: none; + @media (min-width: 820px) { + display: block; + } + .tweetText { - color: #413; + color: #212240; background: #F2F2F2; border: none; width: 25em; @@ -173,7 +209,7 @@ box-shadow: 0.05em 0.1em 0 0 #999; } .tweetButton, .linkedinButton { - color: #413; + color: #212240; background: #F2F2F2; margin-right: 0; &:hover { @@ -189,7 +225,7 @@ .summary .sponsor { font-size: 0.9em; margin-bottom: 4em; - color: #413; + color: #ffa319; a { color: inherit; } diff --git a/front/src/less/main.less b/front/src/less/main.less index 0ea6346..48753a9 100644 --- a/front/src/less/main.less +++ b/front/src/less/main.less @@ -1,5 +1,8 @@ html { - margin: 100px 50px; + margin: 35px 5px; + @media (min-width: 640px) { + margin: 100px 50px; + } } body { @@ -27,16 +30,22 @@ h1 { margin-top: 2em; } .resultsMenu .menuItem { + font-size: 0.8em; display: inline-block; - margin: 1em; - width: 8em; + width: 7em; height: 7em; color: #fff; - border: 2px solid #fff; - border-radius: 0.5em; cursor: pointer; text-decoration: none; + @media (min-width: 640px) { + font-size: 1em; + margin: 1em; + width: 8em; + border: 2px solid #fff; + border-radius: 0.5em; + } + svg { fill: #fff; } diff --git a/front/src/less/rule.less b/front/src/less/rule.less index b5c5827..43cae38 100644 --- a/front/src/less/rule.less +++ b/front/src/less/rule.less @@ -3,23 +3,27 @@ } .rule .ruleTable { - display: table; border-spacing: 1em; width: 90%; margin: 2em auto; background: #f2f2f2; border: 1px dashed #666; border-radius: 0.5em; - > div { - display: table-cell; - vertical-align: middle; - } - .left { - width: 33%; - font-weight: bold; - } - .right { - width: 67%; + + @media (min-width: 820px) { + display: table; + + > div { + display: table-cell; + vertical-align: middle; + } + + .left { + width: 33%; + } + .right { + width: 67%; + } } } @@ -29,7 +33,7 @@ height: 2em; width: 2em; border-radius: 0.5em; - margin: 0 auto 0.5em; + margin: 0 auto 0.25em; } .rule h3 { @@ -75,13 +79,19 @@ border-spacing: 0 0.25em; margin: 0 auto; min-width: 10%; - max-width: 90%; + font-size: 0.875em; + + @media (min-width: 820px) { + max-width: 90%; + font-size: 1em; + } + > div { display: table-row; > div { display: table-cell; background: #f2f2f2; - padding: 0 1em; + padding: 0 0.25em; word-wrap: break-word; word-break: break-all; &:hover { diff --git a/front/src/less/screenshot.less b/front/src/less/screenshot.less index a214659..5bc58e3 100644 --- a/front/src/less/screenshot.less +++ b/front/src/less/screenshot.less @@ -3,6 +3,14 @@ } .screenshot .screenshotWrapper { - font-size: 2.08333333333333em; + font-size: 1.2em; margin-bottom: 0.5em; + + @media (min-width: 420px) { + font-size: 1.6em; + } + + @media (min-width: 640px) { + font-size: 2.08333333333333em; + } } \ No newline at end of file diff --git a/front/src/main.html b/front/src/main.html index 8d53753..5959394 100644 --- a/front/src/main.html +++ b/front/src/main.html @@ -4,6 +4,7 @@ Yellow Lab Tools + diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 06306ca..a564345 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -112,7 +112,7 @@ var policies = { "jsErrors": { "tool": "phantomas", "label": "JavaScript errors", - "message": "

Just to let you know there are some errors on the page.

Please note that some errors only occur in the PhantomJS browser, so you might need to double check on other browsers.

", + "message": "

Just to let you know there are some errors on the page.

", "isOkThreshold": 0, "isBadThreshold": 1, "isAbnormalThreshold": 4, @@ -783,7 +783,7 @@ var policies = { "totalWeight": { "tool": "redownload", "label": "Total weight", - "message": "

The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is already very long to download over a slow connection.

Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.

", + "message": "

The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is already very long to download over a slow connection.

", "isOkThreshold": 1048576, "isBadThreshold": 2621440, "isAbnormalThreshold": 4194304, @@ -793,7 +793,7 @@ var policies = { "imageOptimization": { "tool": "redownload", "label": "Image optimization", - "message": "

This metric measures the number of bytes that could be saved by optimizing images.

Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as Kraken.io or the excellent ImageOptim on Mac. For SVG images, you can use SVGOMG

The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!

Please note that Yellow Lab Tools' engine (PhantomJS) is not compatible with image srcset (unless you use a polyfill). This can lead to incorrect page weight.

", + "message": "

This metric measures the number of bytes that could be saved by optimizing images.

Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as Kraken.io or the excellent ImageOptim on Mac. For SVG images, you can use SVGOMG

The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!

", "isOkThreshold": 20480, "isBadThreshold": 204800, "isAbnormalThreshold": 307200, From 06887003cfc0a0c3fecdb4c5c8ee392c755991d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 6 Oct 2020 00:53:24 +0200 Subject: [PATCH 21/59] lib/tools/phantomas/phantomasWrapper.js --- README.md | 6 +++--- doc/img/YLT-animated.gif | Bin 172675 -> 0 bytes doc/img/home.png | Bin 0 -> 10806 bytes doc/img/logo.png | Bin 20808 -> 8792 bytes doc/img/screenshot.png | Bin 98295 -> 45956 bytes front/src/img/favicon.png | Bin 794 -> 614 bytes front/src/img/logo-large.png | Bin 4193 -> 14666 bytes 7 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 doc/img/YLT-animated.gif create mode 100644 doc/img/home.png diff --git a/README.md b/README.md index 4b541b1..20c1858 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss https://yellowlab.tools - + @@ -58,9 +58,9 @@ Analyzes a webpage and detects **performance** or **front-end code quality** iss ## How it works -The tool loads the given URL via [PhantomJS](http://phantomjs.org/) (a headless browser) and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues. +The tool loads the given URL and collects various metrics and statistics with the help of [Phantomas](https://github.com/macbre/phantomas). These metrics are then categorized and transformed into scores. It also gives in-depth details so developpers can fix the detected issues. -By the way, it's free because we are geeks, not businessmen. All we want is a ★ on GitHub, it will boost our motivation to add more awesome features!!! +By the way, it's free because I am a geek, not businessmen. In return, you can add a ★ on GitHub, it will boost my motivation to add more awesome features!!! ![example dashboard screenshot](./doc/img/screenshot.png) diff --git a/doc/img/YLT-animated.gif b/doc/img/YLT-animated.gif deleted file mode 100644 index 718d072fbc35275ff2bf407a6bc1d16f1207d9be..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 172675 zcmV)UK(N0@Nk%w1VJ!kK0rvm^golWbD`bRPbj*sBoTjYm$i$3qfZVO5Nl8hhhMdc% zykll#O%^+`S7OU>Nwl`K@aW`tcXO1MlYySvitn|pG$VCf0~qyjPsc! z%b2vJnU;ioeRXYW)YjB|MqFqmPVeyVg*9WWWQ^motmNY4q(*$y$0V_)SRB4+uGc;YLlEsb?evDyj@t-bw0eh zyzJ`iynmsUl9c@N^ytvl^r1J5iHbxHG~9qZ+>(rpYJBA8};Hm(;e&lyrvdvZS=Kw5)NKbShNrkt*2Fz|_*zTwr3{!PC{BmDIw_tdOA0 zpt`)Gth$k=aZ*ekN`N+b2et+#{Se!|9v~ri^!qho9I9wq_WnyBKl9YTdTtmxU${QmrRELimN z^qfL;^y2Kid7PAWhrEEIv{_)3HEh(j#LS?%-N4Yqkge3HztqXks91!gQh;P4Oze&% zR2o2pdU>s8jLEsgls$E`Y?Mq+OuS}Pta_ZBdW--70000000000000000000000000 z0000000000A^8LVWB>pFA^!_bMO0HmK~P09E-(WD0000i00000Ednk900{m7wchBGJ>pnsvoKYUO16N&A!MxM zu}Zi~_A1uw53OL8FeQ5TB2$n$+kXC=7wA*HpEnm=EV^~+*Qq_r4h;I|>$swohE^*( zBx?{=Rd?nbJa}H>!g+VLniTfv*wCRDr_FqJ?&hhXKKC57xohXfqy9tRiuvo;w2Cu_ zjm&=ia>jya0lRk*yaWd|I_v*$60xC|`a;j9}r5Fvck3j5OA0HTWoadQCq%FU8Y`?I!B<p&tt{VEIE4IN-N~1Q%p=0hS}UAcUK0w(9Du{;D~>pXd4Uj`eMuh< zRv=112Q0`pLY{j@Ccy#`w2zdjkvuWoxAnEmnB}1OVXRft)uK z5rhN)L;!#fb68PCxhuecfCesH-~$&hL=b@$`V!!RqA$!U@W2EYZ1Ai&Y3e0Ti)jkM zL7KJj0U8wFnN^-IV5tNQbF@J14FbtW5C}r*DQ#ph6cHA1OhRlWidyBTK?E9bp+O58 z6!F0YIWJJd2M@U5LIgoviUy`Pc<_T2T=;N84K7H~f)xi>ZS~byXZ@fMgY8#nL<$eo za<4GJKmZE|{ebZg5EJkLvm)Cx(6fb|0745zNDzVkKq+~90?clH!G#j)!Vp9UNw;tU z7gn@@0|0XZumAuOJn%vU`J&;2zA#8IGyz(NF8b)CSIz3el3JM(2ocn7TEAD;chXcurkrEKd2hsIya&Q0r_~&1>W_{21FvKsI9bt;%lmZGS zcPMcYw_2pB20G7y5PYBnBS^ssTCjo`yr2d%$iWSIu!A7{pa?@q!V#LVgeW|r3RB3! z6}qs6Fnpm5V~D~?&4eM&dKTLTvyzYSk0Aa(jEH82*h3y(L^IA2j}n*2MC>&2iBNQ6 z6t5FS-Bi(vSInXoXQM3s}Ge4Cp`y7_j9nb9qZ#_EML))Fm)& zDa>B}a+tt0rZShw%w{_Cnb3@;G^a_;YBDpK)^sK>v#HE&a`T(uG^RJHInHqI(wyi_ zr#h!uPHwi-oZjRmF^efqCPaY=DE`m^9D2D=e)?0H{d{IW0~%0$uJfP+J*YYp8qR{I zlcC>)s6r?DPlpQhmKKdDMk%UMgL1T?58Y@;JxWlKE|UVZ%x6ATpo4tsfTb&SsYyjD z(Oa&xr7yK`NCb*~(J(vXjlMWf^N&&7$D3pT+EEM=RRW zdX}+{)hcOE%TmcIbG5Dw{;g&w+uE|i_Ohw%>}^?V+THrLv7gN>PGjrZ+;VoaErl&@ zA)DOZI+wT5rEPF+`djE$_qMJ*?sFX*-0WJnw5hEvTpw##CcJF$)hi(M{vYa8ALOP9&rb#iB)9AX1E`N~noGL*M$WbLBX$xF_u zkBOS);Knq}o>g!DnAa>@3b&TZY}PWI!Axg4uldXE6>pch+}t}S_NuQfGneBm=sD}w z#DlJLp$VPn+_sp}RGzJx7ft8c7MQ}zL;*_isplkD*}IPhZe3kG;}@qmv!4ERs%@NO zR-<^z95!{JyBy;J=Xchb4lr^}{OVroxYearv3%LvSq~q$!#XAGlFKSYpV&o!w*xIe^ao25Wbqja8ht}_*zf14xetFAh zX1BRR{AYf98`eH{^mJ?N?(1&%;Ji)jyD!b{x|(~m)sF4F4PNi7nmNw{&-h=jT5yhg zeB-wEH@*JzEpmH*nA=JIc92mGYg-dJy|xzhwJ)CB3)B4OdLA>bY3*|O`n<|67H*9v z%+&tAHQ+zzIj9>>YJcmz=Rk*g%a@vHH1_Mur+(O=Q=RKoPddYMOz!3ay~qVF zvbVo3bq7D4?J*yFVGRuABvw`%U%g3p;LAW1YgA#_`~#F8QmUcIT%TI)aPqd0fle_afFWx04TfphMp0R0nLx zL;gQ}Smzb#%)d6lyN`A88@sOgetws+5BglITKiKTf6y`R{Lb@w#NsFV?OT3)`m>+% zM@MW-CvVATQ*{Sw*d}($XLWnVUIYke$|rX(_g=sEdHp9}W z$bX;siioCwJjZUEhjdGLhOJnHo`{7?H+W`fg?CtXK-hO*hk$m-Y}*HU&-ZWZ6?_8s zX91^(UWa?)b!;(+WTr@k%D9Wow}#&6i@6AQD|n2q$ZNBAeu@`@xoCQLmxJI4jCQwk zz4&y*7j>!EhD7&`o=1;~7=NJ{i{*z|={Sb(*k(P(aC|s;2l-m4)`Xb2j`)as2S$V% zm~9bwf{f>mBiN6~1!DS$kN}B-Xvb!o2aM|2e6x6i@(7PC`H@FRUR2orfcQ9R@^_M8 z*pb%AiB}kO@i=eHRg>byc1DPdp(d0^W_xcHd@Q$ka#&#sXIvzhkS7;;-sg|(XoAT| zeq?Bb^_P#wHg=U5l31C2dN_mqXq8 zmw&f+ovDa4S$b>un*Vr{y7!gzsEL7gjrK^Jf4OI!No039d7J(jcaWKqv3ZL!=XP&c zowu1728Ap6oU=(? z5onCqxtzF0j2@PfP)1bS$Cvl%OmA14KKXowr;!n=WD07W6B?a#IiI-bk!4w(uc(U{ z`HpQldL&w)=lGauDSu>$mjim9u_%mn*LD_IqlZ_B203+QNR)CJqaXTa6Xu{u%8%AL zezv%sk|>vMhLC`FZ*-}PcNn3W2%=^ei7Q!y73y_AD45k}m=H>)#kYtw=$QKVmk_Cw z`q`qa_=#YedKB;iK^C6135Q=th^Q%&TnIhy=Bseo8@|M{S1N{8WBquna1Wr~4-`DoXgfXe!ua@wou_^lxdgfQ8Z&(x!+ zTB;4pZ}oU`4#uL$NUg-le5guycFI?%=%g#i{-8q%pHB*|keaQ0w}iKfmHV2PSc;;q zI%*ipkY$LH=c=lp359nVZ`Mb%wAhJ9+Ir(^egYeor*z$jwz$Zl7)p@*SBgIAU*}4mI4QH7YL7A-q{aES z6Z@KTYO>XutbNO#(Z{nhdz(8pm&|Ief(m#9nx+1_n;eL!3ip%znNpoPtR{+tXhx1q znzof2iWDocA)9~sDW8q0c#amX>j;~$C#D3Og{}&$R%)GhYj`|sgXk)+z}s<`%UlN; zgQeJSbNj9Js(RCDxxGt?oLRStr>cSem9mVPn=xCc%gKlO`in!0x@6g;AN#optArxj zy9~R4?kK5_tC<)UlW=N&BzvO9IH!)OgMC`Ko0^P2IkrNJuQm&}5@?sXYOiamwJ_(f zxT|TN`+IC#q|eH@2^)?R{Jc)djYo^7)mn6Ddb4^trj)yn|0^n4SK3p3V~}$X3AEs1_*As*R;n|+)Bi_im=tY zfhV@SLdk=Exwelgpq<#FT&tF^nYD75u^sB1^r@@6%DL02uTLty*lM8Gn!^m~wIIB8 ziTH6~>b*{!jh&~&>ZO!io4)?@JGZn-V51wyTWg#1d!1D}oSW&R&PROrJ6>g)y7%as zPi(xR8HYFdc$DjwT1u^m*N&FDv=M2^0cyc3E5}v3nsy76NjqF&TB|>7$Z09Hn|Qr< z+NeCpv}+u~bQpqAdztVTxoU}-S*o#)?1JtI#liWKJqxjy?4|!3V(t679?YuYryo346z3Iuowo0xL`Go@+&8m!P)ke6`Ii=p^j1cO+?0KB6+Jm25kL-KI zylkOqiG7yY#F?yvJLkdhtE%CL$a%V<0t<1<%6$e6%g}ehk13tGHkHEsj*+&PY|G05 zjCl{OcGC#I100=ZYW{}Ti(c?7!dk3<%e!v69EQ8Mknw1ST^z8iIe#`v$?cqj(-^@N zN|8%!g_=iw7CNlesL0xx&8$bR@q4ILCdHjxw*gv`(#OVB`;YEixC?r)ms-0Ls%m~r zx4xXi6B>-9dVd|-djPwgWURRU{F#~g!N7~Z<4nsUd3UN+vF-ZE{Y=W->47o5y_I>7 zcO0BusdXM*fzTVuS~`W;h@K#ZTMMbZk|xmYN72Prn8JH@seGuHt&Kgal3m%>0~x7nt#>h@e}}Aw8%LYLGlUi0vuHuuOnUX^Omx z)!~S@08NcO{*8XC_;mEl*$RDehN#Kjnvo*ba6V|CzuVWBT5{ya)1}>!ORU;;cyeQ7W9fV;>!i3zW+S_TLdVjHr zonDDpDgJL&yoXHJp)rfWHi^?9nA{o~c#?Xb=*hSPzS|sYu#yLCYX~!tA99_W9jn*TL;t48bI`zvSyybcPE3I&M9SuS%$g5plT*ITE<9g_uFl53Sp_2pEVxdL6- z)!mfn=A@+9)ri2<7vL;0&*jO~nHn&;wa;1fZ+}96#%JxbG^StWbw!ut@&2 zO)c+pH{p@amb{2~2v)+4cie!w%hQaFe5LCvD&+h4zDwTeAxV-`46QU?f1v)s`3K3o zX2Yzl-6-G%VQ>cd!~tFa^~GiZW`F_~5Cey)U1tym10aMtFaQN01-*BPI-v0m-~jTK z171)DW#ILt-d$cW1a5HQkWP)w{@E;h@>`0p2t2AP9poxLgqdvDh(6i)$6bQupN_7F zL;vA+u6_2bVgk6^zSq5yJ($BM^tLs^M!Nab4CtB_0~b&R0k8%aFaZlzSri}y5O4;r z-&nNOhOXrX7eE6azhoS60dmmsSISao&jWe3`}r{e$;I(K`BGhQ0aEZ;eEzNT2k*{w zys6AS=_DKaFO8@h`sb7T%&q9AJs!WOY*lp{WFZUNGw}re=E9Q_Ks+KG`4K}F9XyVx(7_R?nPWj{zq(%vCSq!D0`){{8H=Ghwis>B4=> z8EodXbHDy2y;!r~uwylk-CDPERfdWGp$PL_G!$7mTLoQ2O8z9zf}XMR}Y8LJmTZPpC?V{*a@g zsu-w}!oQ*Ei!9eZ^AN?@ z6tgQu#ZW?li5Du9%S6_8`>V&GbSo{h*<1wdL(FDGva1duBWyF#BGk=6pawH7MFs(!W;Rhj@qTqrwJ|e@UCyu+b zy`#$Tud2XQ;DINK8aP5kf?&!{h%gR>0E0Eso7BBC4$MK!0^~4_LkJxlOfV4f%Ztmo zQthzA8Q=O$Fy0tr>%`DPEb>F#Mog_XSl^oPu_Y;BK_nj;^i4=$cSV&cQ(fCJRI^mv z3PZnGZE;z-CTx&PZVj_GNO2|PwaCO|>Y^eq$%uhEAt)FDq915*Lx|{9a7YLp${bLL z_RPqw1d|S8ZiF)sf;ED)3LyGi`p?#s2}QGoL%xnFE^Y)I6%;W;QkcWo*|s!3>V-;0jDScgdD(cM4ThiXmaN>@g)y?Pr@Dzjn|i{{m4SALYk_I z7n!jEkBD27TC0wPJQOsIhg*75+Df#v@a-;aNs3)&l69FPvaD!5`{IYr=e(c|afyV1 zS%or$#uu@reQ{||bC3fB6a=S!`g+?1elSO%$OK>q3PWS^R2?8>$34hVm`u{MM~E=x z2o=0wB-Ox%*o+WgMp2KMbYQ>>i~u zmLw#`w7~E*InhfF$RG!^$YBU47y>y_FoiN;zzh*n0T6~jgDx0O1u29{3f({#3bcz| zQh%1J1T`< zdBmX|rk6SFQK*j8#@wK2^53)WvU zt*9xoX;o?FDi%HVdTy2G@Eki;F&?I`vboG|lA0}RY1UN7dR0wXnmmks^_5=ztG8bF z*U(gi4Lta5a2>#g2Eai@PwVZHh^Zsm5wo?*jA?|N+ShJrsk4pg=5nX!(^Fn)dRWt) zF=fT0!{Rb3%nNU6r8cwYLe@7Rf?aRUCc9d-lCm{rX3eH*HmkuZvXO0;W^;(t+a8s_ zr_EKV3S3&_$_J=@5n_MMl~d*hb-lten_o{VLkpATW>&;-PGJXKzf#LrTb+3Tqd99Uf*_ z8Z29H9vQ}doh>X2yUN_sv~AoBvU#b<82?6?z`yD&i^51)&pMC0mHi5OT^ZytTPeg0 zx=nRqwo?I5uR%&AaZ9^c<*I`Csqj@W?;b?w49Azg%oVg3KU?RXqPE5~Ev;0!=Tt>! z_PSbpSvA?ByvH`x$ZQm>P@mGUf0;-@)qxqty#W?+xf4#ME6X!+2|N!&olg9c=Q}?ecVz?Yw;KIvk^ei`Kq@z2 zeYsx+pIY5Q)-L%BJ3jR`6TqMaA}{M}OJ$wWL&ol*uWEE7QvXowREN2Z^Q~$`r@h3t zee~9a2I#>4xrbT>ENjM2Oo^lV>xaF&GfhL`%@&hYE2X@`>k3m1b13CA%&{t4I%2lF z{%nRb8a?IDI&AvVj%L-ewUMr+W)lB>!+s}t&u`gw-Zy;cME88KqFHhxDRuKL?=I~_ z9%;PO9;K;-Hr8do@BP@myv%cu-;3Fox7@?RcDA&2Bfe%?s`s=1*`>;fF zwa0_4Wh*&w`>D!PG-fNK|KmVpt1Bw=B@wK)6A_@IGOXj{E}-i@NL##4vmuR(t^?#e zor9`%vz-}}rnqnzwo0E3EWui#I3oPJnF%_?prvQ)8Y}8CXIV8i8>5Qjxhd-YH479Q z9CVQ?TdcKuA<^O-Y+E1x6EmTkzToq|ut2Y@lD#WozVsVI8+*Obi##@5uxL_0aN0RT zW4QRMh&8YP3!tN78UP?@7`RA`0l)w<8;KlXh>92hZ&HCLIG83mh#UYDb3v=9GmCoy zf-nG`#E3aEE0HG{0PJd=s&OTxIjB%17o#&c6se};f;DNwp;a<95hFDtTe9tYL1mLH z>cT%tqpa9lt)`kkB-F)BOTFbvGjbC?;gGhjvVj^3y^{w zaEUdDiiOyU2davzIG6^2iZBQRlqi6ta095&iWrarDZl`P7)KS5gE0PRA}QF9$IwRy zk^<0Kj)N?YmKX{t_{Weq14nF#HK3@9z<@=($Z}+;gD8L)$N>xp3W>y*94H_P*hdN| zARV9|6j%ThuFT;W+KnMWa+W zWegrGjH)2=yfJgZg_}DU3$_8|Jek?OJS(}u$~B|o%BwQUSrQu6x-b%?taY;~6lkD^ z=z=w%fiNh55-v?xnKkGF4j=H;@g9(t!gDe2T#6&XSfc`F^fHinbj6#Zm>H?c^ zlOZrD6&L_e;ea(DgDwaIl0t|W5P}hyfa#zC2nYiTpaJ!uO)_{6GB^o#!Iv0#m?yAI zDHzOc3{4Iw00gp4(To8OKmjr!s5GDe3m8udD1$OM01l9X#~d%-3Cb7bDw|2M%HyKW z+CRnfz0j&puyd)x;5NM2!kJsa*-iN*gSIv(NjpPu26dWduJIxQ?)h zfhX7wyEKC*;D8u_i5QRrl?j6(V9Am>gCQsbIT*|hokUQfh&b`hB?u%Gz(*CBf+tX! zgnELzgb6YM3J!>vGFXEED1jI-gKnIHCH>IrJQy1-{sSQB0;?dXb*#-FeHbcrg8%>m zfKh=iD1a7vg5j6|AuxkA34;O|fsLYoA5a~b(19r(g4hg$GUx)$G=nhk7z!W*_9+8| zK+Ql36ApD4G5`V^0MQ|!(noAV>mxnniypJ;D&6DHybG+4Te4lmRDOHV8k{AUaSIBd zyH=|*HhaI6GC0c1yR@+-ohdZ}loDBuF(gd6WMqmMh#XyQm?a=m5H$mlcv1=|0qU%X zV67(#kOPmjZ)` zU?J7?$2Fo1wF5Lg{JjtDxCbh$(b z-~bMQ0Q5B1d~8vCxek9+0H7GmiwzSwh?{OK0F(sJ5-5O!xxJn_HU&hxeQPeViLX{- zx=)2RF%ml}%r^ZZk=vLIkuajS`%fJzv87zVvE-Q&>^#q4%Eb^quY^@bQ?W9N3pXf$ zg6e|6iJ;7EQ4ZaYV11|W+=?;?oG!pqx~wN40F{dHTIR1E$Galwr{!FbMt?SOVBwnZt}*3Ye5Mr2xJB&cuv?m;5XliOR{- z)EK!yX1utY(UvDPSlO{YbkQ;EE`Ll0t!<+{mtofl@*UFp5YDpx=nx zNPpx2H=y8gGkqHOCS%HS-A*9 zx;O(4<$wYn3FG2UT~ePl`Unm%6a-b0Dh4MEY;Z7~%?GE2POm#Q8iIw|>kxZM#nKeWUQOEo;hBHlHyQYo4!Th&B% zt38w@xEl#jofen!GK=d9%!-lKLOSFsG~G$VG9)*FYdAvdu;Rs%oiUa949hE%z&UF( zrDQ(bOG6sY3R2bCw0tsQBrOPByJDc;R*dA?J3YH|I2!h5L#DF{9okkYG9MwU zo(`)gKEZY)yem95ZJV)*=H(F!Jahx;@+-JgU0Hz+D?r}n)%w|q^PQLy5{=f~>#gaW znQE%?s+rc*5bR<5TfN#?L*w;NIHu~4V5wbxMWfNerG7oNlS*10%c)ypw?j}Rdb3%M zUgSfG8~_6To!u6?5K4zBfiPSvp*#pwTy3%niaxdsqzfAX1S7ILn#PMeXY^z;j6+tF zWEfUU`AEQ*Ldy_D!JN7oOf|+;3ALL}w&sPfVeCPjwlE(Cyxzm-;?~2!F4|!`)-XuTyWCSEC4;2+(h%L#F{#TT0394h;UQQ8 z6bAm=^du;cz|3+DSQOw`zYK#XSc5&Sr-)&(`8$;^+{()?mz_N-m{V+yjzUN7ZSqRw zieoGWoL*ckHwa1UBAiN{^1`_WL-Io^kkh(b5xV#~zp1%K1Ekd>N@hycGD7Dwm@1Jn zC;=&W0y!YjGAMv22m?@Y^wja%D}RCjD1(obPBO7qbBX~eRabdUPlhmq&%H|oK9~|f zauhzqri?*K7B!p}IFRe?}}s(VphYxca~)@*S`vT@Lm#)!p>nO-oo62Ka{gR zhy%S~7q%fWyKI6=xt5lm&ca1DDrdyG$##o0&H*`~fITfF1#t9x3i5~rf^r>#{r(`* zF>w<|Phh)LSA&rBFP&0-ErTZj?evs_h7ALh0cO^VV%-a5^7^?g)4*v*?Jq}g^!Yzn z!tiH=YAs{h2s}zxmAPVB#jfG#-2U~8lWu}uzNRMX;RfqI=Rq$2s!kp6qFbX;K5%ai zp^8XJbur2Bb^|qTZ`KX^*On#>5Q&{936P|S0GHsVA803*sr2gPT{BvS-{`i|%G*xL zH#FYLmSkeyzS$GdT=L`QV;c`Ylngv{vV&_!8*z!__}IuX-*al$7W4^KT4fn(W(Mjb zi)XEiHZ9ylX`$Z=sl@FLgphL4gI6mNR$m9EmU^&V*2ZhU7@~qsyWt3sPnN*-2u4Jq6EzPbwalUj56)Vu3Oo#eRnbo1(v@#dUoLKko+o)-4P94ftaa*rEnWjYy zb7fPFEE|s{Z1?hHj*eqS6%BK0%7;u|B=sv8GDsa-G0z-|S2oF~BN2bZJ(z1|zN9hQ zbd7m4@|ux7(&c@UxmMe>P3wkB8+UW;fyqApTOBiMsd=yK48L)_dCCnT7oSf2aN^KG zjRWs}-gMmai@hsFd%Ln;VzuYlPXrxBL4C$ich!3ReFvOdxrId(N$uU&RY+^Sc2QyF zZI_{7`}r2xQqq;>l6BmTRiR!aHdfn-*BRDdVkk<-nQ#8)eHUI;f!X$4R)m4q*Mn+} zC?bkNVK^UvbAh%{ktupe(SN$dW|Cf5f+yQ*t+AJ$W(mUe+=MZB_yOAa%!rZMj%mBi^s zXsD{9w_=1^+J_%n+hs{3sJikQ9kO-q#iXJHHVWchh(dSaV5d=NUx+PEmY-ygzKN=* z$4<)XXX*8bWp#|Lh}UHUb%|+y)}fkXwO!78TaRGuL->nn@XrK;?s@?K13Mhjn9FABoKHl>N$RaI85PIg*r#R1gCFN+_GsYu8sA$fRb<{&0VB)1>$2Z@Jovh zm8|BaNk}#%nF~AUMM2*0mGd)HcV2QmOx3M|WUAz|l;a$Jg~@bdqmwaxr$aFgYl?P* zrm*Pa%h@Flo~%QhzyPL8|DEwLO_Utro`<{{K}v(e>E?}I1BAB~}< zUj6Q&?%6YyVa^FNd)8Jh>#4@Y90~I8F?!#X#}fMleLYc4o^tKD8)3IT8x@sCz-9^ z3YLsa{#LJn4{by`2fD;8!!c424QBgNN!{QmvEKTFV0%Wjr2 zwiBTs+$2gCTfE^sQ$wY6t(P0 zI@n2r_O`cO?r^6&+v$$?x!3*fSAS#Q!%lU*%YE#3Py6BD-gUscJ#C_jyVwC=*Z)IyXP<8+&;90qpZDKq|L)gce)KQj{9PD7{F(p$^rOH0%iq5Bx!>@`00%G= zLNve{41fYIAQCK~13n-GMxX>P;2N;OWBCzc#opG4N#MvF^$5~Tp6-uENX5kcSAr*Gv7J?xant%nw!5VztK(LR7RNQZ5 z%x-PWd?}Pl(HVSQ*&qQ=pRkvkX`WlHntZVv{M4I5cJ7RnIecGmJFWPwHX?lx!g7>$FJbxO;{ALDG0L}3s*IctF6qIsDKw7 zBR=M%KJFtw>L3n~fJiW76qbP{UZXQUqbDXLG-9F&P{A`X6F|X~pXiBd+1tR#Azgvi zvmuB9UZ5^njYkewjuqGd`I!e5hzbCs4gSCn+~5ebBo6o>4hX>oC_zgK;}6WF67(Za z_M}hRAPzV|%t0YR9zX$@03}M|0VriODrFQZr3DngQcB?gm_PwoqEkxZ5JY1fFabd3 z6Ge4XF%e6=QRSG_gt@R4}d{lD@{zXZs=TDpyxr6b`0Mi9XiNIIFzVa0_7+L}#I9r;VEpc{E% zjku&xdS)7&=;aCc01mK#5WK(({D2QA0SiRI2@s|d#3WxX<_~Pa1rX+7z5rmx=*#?a=@P48d*Ezz_^U8jt~x(!f<3B@M&?k*2|s zDk+kZ!6cf1bk<13)J&`~UGuEXwn)m)sa|?7Lpt z4jd+avM7U&W{3jiV*+Y`J}98VD5=6`P=b{}MqwHt4W57%j6e!7=m=PW5`cgY*q{$= z!4JT|O%CW@?j(Yqzymy{r1Iqm5URa?suDOV4ut5Q{^=IbB%hWl#`5GA%x3IC;U&@l zbQWnG;AjD)!2#?*$=a%rMqyN@!K|jiBw7H-LcqxG02zX;jaFe2e5FwRk_4rWk9=I2 zoS2d6;f_SDkV>uz>_Es`fRNrQ=8o(TTp{KjfRIWl<=X5OR>75~*qQaL z9>LtYog2*^RkbplnUvsAO>6JqOJ1aXC|-V zHm^?x<=jxfHGX0lQlb?uBpHIDK}O+kN+EP+VjMUm?i!R=k=BRkoVAIoAbOPcWanCD z7U#*-F=eN2mE4j@S*LwetLYg~JV6jrCO!hN^A0cpUqA}T=ue!%6&B?|f}#UAa05H= zGe%<>nC@2=QJQ?)I7OAS{@#j3@{sF%izsnPW?9#}8Ar95(!>?kibdE(CeB)w1R)kM z4cG8%f&mr?6bc{#RaPPc|M2uq??6&zQA#j!Qlk?r>zmq4HT6}>wWrOjVtEA@3;K?B zo}--ou61hU?Mzmi$_}ITYyeRN2iUL~pK(5ZK?Nl4aj5_WFz#>uW))=N7niTf4;;)2UJvRmv^F4|=X{ z3oir-2mly}fhdnMDVMS-pE4?^vMR4KE4Q*MzcMVxvMkRsE!T1@8^8=yfH%TH2r$7F z{PGo0K`;-qFc&laFds8A3-d24^D;NHFf&02%z%t4ONK!fSu%wv{n!S-ZtX0w3UNhv zMs0Pu6l?vL6Y}DR0ak(Wm)fv1L6Cq5K*0>mfIQRlJl8Wm-?Kj3Ge75ZKku_Y2Q)wr z^gkE0KpQkcAGAUzG(#h_Lof70H#9^~v_ubdJy!rBtJNFWb3MFu4PXTdJX2D}ONfrB$FV48mf3dEGKqP$-#B>(54j463 zAGJ~^HB&FOQ#UnKKebdxHC0cwRaZ4uU$s_eHCJ!7S9dj7A2k&jL{lp@S-=5Ohrm%6 zHCnGVTRZ-BLu9n;p<^$VVg_fMEK<-rw%iNTkcCA~dpQ+Zmd0}-R0tOnVW(CcPETSz zNlaK5Uq`lNPc~&&wq;*7W@ol$Z#HLNws>kW)@bMKK=!0C&9(aBd)Zmu1dVs?+wHm| zPaKWz!pT#~m87(9Jgs&KEfU-0#O;+iRO;AF9xw)ix?u`tGA)4kkwxpBwu)Hsdbcz4@) zj^ntF_c)LL_>Kp;j|(}F54n*SIg%5(lGhLZ&w#!LpG6GGK*A}=wVkPoYQ+mwL~>8vA=v19dkiekq(l%2Dj+j*V8&z|q#_iYzMhNVlcB52@{hmjtwK_?>x~#wSth0Kpx74i*S+3v3uA@k=kDJIX zQU04B_Ex+(;;7<;<&w~C51O00(R!Prg!2~HQVtczh*|nw4R(F26jY5|QyKOj`BdnI zF}aIrZLC^A|RU64ft7PA;>30ph8cqeb$?Q;8#VYiv}STaexwfGwhD$mi& zZqlz;(Jy_EHNCYzJ=Mz!)L(toD}B~eXN^nP%lPz>Sn_M1=@Q+?J%zlN&a}l*{<=p+ zG6+988^z;hkz5OV<6;3lPle!i61#eYu-_Nd-}gP>55C|RzIhV<;T!&0DL$AczT-d0 z>o!@R?N!7Ty!t{-qjm9H?RD$L+d7}%<=bHlxz8y9uXpapu}vH+u~p}t@Ws7;Qo+8q z%|7YJzU|X~Un86D)7H`{D3KwcIG0s%YN{j{ohKzx(Gt@T))jYnwZ#Sy15> z%LRQ{rIk<>)!nZkK`0O2!jcaULR4Y#5l(Hs6WF3+|Q@@ z_$v^>0|x}qI0qjy(7y)-RItGcDU|R!)ugJ4A@^7+&N9fXatJlzP|_^Gzb4{NH{hOQ zi727uvPm%(IAU?M+dQg~CZOip??2vR1g*sZt1IfR3}Nig#E71REy^E1gtAI0ucT5v zk*4G_OE0%1p@#lq{(EZ+9~Og^JjJgvSE(JYO_BA3JJO~arw??H?jBz4G@PF+yGR7*v5 zR96p#Rk>DGrHs{EU#(TtSal`Uwhg5d&CLbteAB8qm;PEXwIt8-jW+E9#dNBzW}5ND z)^tJvJUp3W)~-(#?ex|4knJkizzDtU-1`VacQRqoL)KlRq@@>LBm0{--h89ASKfa2 z?GRvm1+F(>t_;QyPP3jB54{N08|$uFD|HlDN2^t8-3?>SuPxI~#k3$sJ+!f}=B5-c zHXZ8&F4X%_%<0i1VRr9NMAJhwL;+``Z#18K=GkGLb3U)=pM|!L=#P@#dFk$qX8LKM zhmIO*q)AhCt#e;P(A2mB6?fBz9bWOX60a03W-1GgbhCSn)AohN*eZ`tJ@tCBrw?oO zv^Pk9>oe4^p>+~lz5rX-<*XTg{Bp_{x4h@fA=i_^9UIoI z;)oGHTT)>Q2DfFwv*ei9eev!ZRZL0du}Ek6CH1G|)=t=Pv}H$iIfS^O3`&HX%QxM&s8e9-a!SwcwWrD7?taOrX1d}4GrBJOC5l@Vj2MAR=W z<|>TXO5+mScqxoffDBTg00)2&135@R2U6fb33AAR9H3wTQb2(m27m#BSRf2$c)|)P z@Pr)}L=HIsLJs6m$pN^b1Q@_T4$OdpHApfCLnwh8o*;!BJ|PSomSo815YS01S!N|4Kc`p1?1q* z7(Ag1IVgh}6kvveC_oNfU?u(#$iM+Tsz3^4$bkaFFa-`200?F%0|gA)$}mV_0ww5z z6ow!Nj>bR=X1M1WSn5)lmZ1!8H~1|mIGZpW+Mmbv5w zN`VrTAq+8~fC_3F1|V=WmSG4$4qRCRGX!9>1$e@1OUVH#&@=!lpuhy=@K~=&POfvR zt8Y6t%{KXONj{=0^#1&pHNh3CW|6~ARp#_7@7RuCOUWI*rmI%=5ipuBl2dV&c%8jv zSDi{7>u&IL*0(KCY@qs>j1KyQ7y!YQcywe8N-$8CHjZFHn1KUixF-fUoM6J10SG#Xfg1)~1`d=#3auIrYp0j2Z!o?avzTQhW|iph3_57DK`0=FGpOJUa_(cBOE5@* zC=vrpG01{b{vziyprFrfZgXYbe9I6fy3URHr5oP-%TCtPmP6^Y6e)}xW&D0Rr3+mYi>lhc-_rKY4&0QU)Mghj@NkY zy)puE$f>jXm`OJK%~-YO?4UWQuGM=ggUa_-p22~6b~{;rxZUkg+x6Li6E-}hCS_O| zJ3CixCa=0{W-jS2R5lw?s<;bPrZl@O`6KtY;oUTRrJ2jpOtQV2x-Ra7Y%HgnF~i3r zkv#QCQZkyj#4FyTM3E(SJ{*$8gwkCyG7W*r-C2?@o-F9i3_|FpOvMU_7$`>EY{4uL zl@;awa8_fvL-7%t+CXGouXb6~o%2f0wmj?SxCou&0ac|w&8H)R4QJ`4u0pP9Hv)E% z>&S6dt5L$^X8BGf*k!nzj^`+`YEmp48s`T!>3Gc|<0=y5oel zjG1?gEa|F~(O1r~g<5H67Uo+5UYS^DWyUx*+mRphD=!u%V^qz{=Vu0z@G7b~WR9E1 zhMUZa3t6hng#0l<|8SR+PpjuECsikPuQDkYs=-X&Q`Yw-Ed!AB-c1Pwx=HlmX)~@# z!F%nqTDxm^uH;9m&h7h5=vXewn2Tnj3S97v%@DA$`1Ye{CAS*&L zWGpa14gks7W+|v917av72S%t_bmr{tZ{yBveXfkGR*n*DC*HuqLt1+pMN$nO_@5hjkH z0eVIb+Dv-HPOd1R#C$CWRLRraDkh$Q19gv`fKGW%k(?w(T^!E&tf>vhOx~`Ct+WgN z_)P<+$^f&FG2SJuU;^@%2n_Rt38$sE4)Kf%NHWw#JKW~`$ORSs#2?G+@u1=e`s@!G z4k3myG~}Sq=8e$K00ScO3@B2O-Yp@{00^q11o-IIAd&(oG9uCLAsRAtsB02UZj5y7 z{Vrxw((T4tMc_Ikj`9rvYmM1VhS%^f0h!K-uxt26N9$0A%7PFo{xZhvfUbAaZp*Z> zC(TUz+;H|PPd*m1JroHAu(AON4J)~F&_ZMw4`Tq1@gPco287@&(Q*XNasr<5B9`$m za=^f7<_v~F2!L3EIprRKN@_;0I{n1ZaRbX@CYs-~wVmIs$P|oKZYf=mH?%F7a{# z^wJ?oP6vQMJF#gAaSqs^Z$iM4-HveiD0BEGh74#VHa8$bC3Hd~ln8WFAulv0jL{~9 z69}-9)A|eum{UefP-4x%byf;LkyOO8Md*x)L_a@ADGAuXaqzeWW_v^j0! z3<@MR1QSc6ByX+5mwyT0Hs^QDmkQVNB)IH635eAcZUcUuhi0QUObP>{0|GGWjSKA z0U&}lsn!g2U{`x}K7KVQHWX?))NYRTn3UBr{IrAyK%u}UT6JwX`p^Rpa|t7LGV8BP zEtBoQ3L&Ac0SV4+sxC0^E+BW0MKXdNKW6}mFxY0)_s9mwT(fQRjea19NB|D$%qUHs z@YVdbnhX{vaMgSkmKcE|PJL`n)ox-n(kPFWIW#sf;_@c|6)9S@G)xy*Ytm3bRccK$ z-%`&&(S=%66&o#YQG2gskjG_IXK;BA>qfC=p)ghZFq&~Z;^0HnM>tb)%8}B^8PUc^HJ{tm?*T+;1E=9^wYP@;)HTcwV4k#{&X_OQ*!v!k3X$o7ee`BQtz`G{GYfcaa*aU~ z7&?;9c?;NpQ0H%QgKriFyase=r!qzeZ&m@WgG(_{rq*|8lL(CYn34II8x}*WV~}TL zVkhY&dUz+6abo4>!vOEG1lyMT1&v{_+nQ8<#%mDB|vm+k;(HrTG1H zfDL%yqS>Gah#(6t8l!n2S6lce+;=31^?r;sPwC}34cVc28fCeUD0?c3n z@EQX0lSH;IJlurhgMp-nMvA*?Oh4?$8W*8xK5b@yo#Up(h6HVwtl~ z(dZ!iWnAPknJ{y+CzAmaWi5N%RE?<*xMAX1l-G$Wcvp5LC~=>oOn0OKFn|tw8)X7H z>^2l4azHOhs$XjhipJUgoY^<>rtj;q&+ z!g@DdIDObOom8M}HB^zxLop~|!rfrP&j2Kc_J^fqunBI$Jsc$M#35ib=v=c1P3OF% znw->d5@(f-ScVFf?&j!;SrnFBPt(-Qv0eSv8Y_`|(F+Z&BcP9{^1|x9LwT60n3Nar zN8rs;+f%o()M-Y9bd!Rv>$&{OMp=W!e$V-8f{Qz~)6{AtK&%+|;xDdR`GHFWT$;vZ zZS<4JYU}C|gN2K;ZD%obO)fmrnk1N{9 z*$J&~7bPTCy$^jw&)?h?nBO_cbC(+*dey%!Q@WU+=a$}Nbbu(gU#ILwXN6;8^Fxln z0eWC5h_SbMKnE^b2Xs?(8G<*xcCiH_2RN#hP(ZQjOwQau2R`aJG<;a3dmuKfmE^z; zY)i(@pacwp-)#a$ALi3YFM6X8)>r#eoxBa5^vqe2ye5;kKKn=z)5d}JjXxxJuq8lO zVua^$b2azif*NIMJA8eFyvuO{;SoWXEYkl?)v6K&96$wlpbdJU2dte3Y~BI>bbjY+ zUUXYnbF6$N!r;OI^S6 zij}NoPYOkeTUmccPgxjHfhJcIcU z$a{T_k$;o5y8-3K(dPT(C(e6=1AEd4JaNvrN`9fG&w2^HpusGiZM*&!A2#Q8pbZ?L z0UY3>9e@oQVCH$i+S_0+sN5oq@oS86#r(^qq=3G{fDZxUjE;c=&j3+@LWhch3rk2@ zh)@TGgE?|AS;N5$5FjYz$O$8dAiCm-B~m!QJyA+;_Qin0t+Tgotpg_6J^<{ zWuxLei&bmfxlp}El&jS4TDmWLE?gVcs!pg8(O*` zjXSBzr8LpLt$o~a-@dn<7Y@v`E@-i2>;5+WcKGp{yQyA(DHuH4oQS;x2k(Bp-oaaW zmsa~d{m$|}fxp#!7em?6HWYh004Nm@6a?6mbZ&8$kU`>=u!eGN2`F7$Pt=86O6I{u zm~hYWCsJd+^#>n%^@*obbJYpPly$w~H`{qL-Zi6v)QwkDNld&zR!SG*wIhuU@>Sn= zqWK5kO?$PsB3E8XX%~L)jW`&U$?3NomPD+RBfyoEM^wdAOXW4=M#^e-ICfD{(nv$fsiJ z+8l4rJsaBU#ajLwa-#&R?@<8`Xoh zYb(2BqpKZYKV#=4&=m`nqM}t@ZKIsH-N+QIYco9S#4c(2^xL2UJJ-O!>ZWMqXm`m~ zz7I1g_*UzZ*lUX*H=HHn;pO_F!irG{kHmDfA%Ud+Ui|E|f zHIPXudcw0C-zHW)@9>36n%f)Tlwo5X*V01DkHkw%v zU`y24mm)S5g6Z&X^6_F9LBpc$Wp7^{a}Ma_Q>!?gC`2=h6T|K|y~`2sZqa+)k4z;- z4RSA7%Bi0G)_Ar`Ws!Rh`d`tC$16S|W{wrRW95Pdv=_PxV_$R9NkpVbG=}dim2@HK zTBkwQNUm{O6C`!&2&@h=vUVKYpCVbvxKtVriy&%T7EQ^v8@+973ZxtfS=Y*;!R>sC zgVxRJ#k8)}uP{#(S?^GUG5!-;5p2JD-i3-8MM~lcufvE>|9>Ls;0{$-cNe2^e8~_r%+}3jiR6| zj~H2cK1<1ypCOE=lX56ROICBILPaSQt7O7KE~$`vD`TYKC_bFVv7KH^5yh%0REUPM zmYmuqO%u4O6f%*JzZ7KbGATdYtqOfSE!r5%N)#(TQl7~xpj}6X&zOlaS44b{TLgnT zKr)JrP*Y)}+~!Q4{+>#i>Z_@B7@Hf`wS{9M_1A602~xREP+t^-8(ljY*Osd9K2qHs zTkk~^scmU~G{vbqkz~hB?kbv=Qx_K#r#%y??W|CYZ1Ri(zwX=z4OWKqTJaOWl0;Z7<`>m}dY`sqB~ zu5g#+b*7N0>bQ=|5^}K`uuBOflN{PFR+*J57YiIX>RmEoLK7AK>Ib!yCNpmyMc^?h zyx&hsj!LQGaDPc^Pdn-en&Q*&W0A;GFfKGY{WaiPH>y^*eRRLX8st7F94g6%lXCs@ z)%#KxO}9$^@U6MBUXmXwVveejquyJoa8(!NcCPcCWP=fclN8SuH*thO=B8ks=sgfB z^}`pnA4#E$I~dY;NakeeSX;L-0P2^oW}978h*(}W3pk!=#F1DOt78Tiu|QT6?J->| zr5dJfZ)N6gm1}A~{JL0hGbZJq85`ZL?zXux?KE*^8=5Ko^~?^$Bl;w$L-VSY$AN5K z*NVC}^UkEpt~Q>x5GqMqI!w|!I4E6)@m6N*O+w++=%^YkQX!x_|;dubu%CyEKQT~it@CWc$iRr^djfm(@+W!0XbZz$?c zJ&OLAhRjbH2m0wjZadt_)SXG=El5C}zum5fhqsMV`13O=*M-waQJexBw~ zg!J(UP4A70Nw7Mhm_ z4_NKMv#4g*o<5f>1qq&H|rEg1pXy> z!W!Hlg(ryp2~WU)8-%ZeHOPSs7(l@qq;Lk`W1s+XD8Le=u!bj408ayO!}F_h03o;` zA#ON8`nXHj8nWM*eS=6-KQ zDFjGr3V3et1~0(2Zy?rasxof}r%_#$UYMghvZoMo&;>Do0cIctF~9*~00d!R201{2 z1powP5C#Jv1wc>$VbBFn5C&2(1UhgA1t0}xPzGcm2O>BHHrNGgpa5k61R2o<2cZx{ zZ~+$}1Qk*Od$((B#$`e#UTr0Lp9Nx%LOcDIUHF1+z=2YyrAH7be{2BWncyvfQnL32DUK( z6L0}$;0ACog>cq>#4}`^)@-u{hys^k_lU6@i20Vf%b4G+k-u7m)rvh!_8c!exnV16<5GZ;V z0~?_Oa?pc`XoDO;0cDVhI#>oAAd6*ykKh6*QqYg}NCpHM20S7D2RWz^lQ?D((^-a9 zbIVv#D`IUF2u@&yPqg)O&(uPsMRkG2iyX9AX-G^m(Kel=VZxC$tf7rTXGsuZJEx~{ z*T!qUqc6m>St@yW$4DrWQc(zoGhr~1t8p13aTy*Gl@%vSlA#iEpa5o20$0fp96*J% zc3(`{JmThLs-s>Q2W2U_V_Ts>Q8sm`!!_T?c)RmnRh1TArWiY^ zkyc_MBy?7<^pQET8*mw3fC)=k_j8)$Z1gi40wHpCqd8roB+VfbdJ;U2at5E_T3M)5 zt!HZsgm5y|Gz;`%C6j=m_g3j-WBfOCOPNtxr-kNZZgrMoz{wU%yh6 zmFG(c#G7=sYF~DBVWvkglxGa5D;9`Xg=l8_2YAK=E471hgOPJUH;B^JnVh9;XH;M% zWoB0>UykBXiRXZ<(uZ@In2M8>HYXkxG$IOyVkS3eyhWZ|XK+PA!kgP@r* zLz%*0s?=svcT=zG5+9?jKchS*k|32P1YPtS##DxyQEw`1SsS^g2IXbQ)mD0FVH88(=9RPiH4g&3mV z7-^UnL;aE$UDtVsvu_@XlthB3p@W=~f_YTeQi{oJM#+(xcY(G^RXqxbM`~)M6lH=M zT@NT(pM;@PI&~E#AItWBVU?yu1X-=ufC5S$z&Tm$RGtWTU`Yj@j&rG$6^L|NZ;_-% zIq5lR<#gBCa8GlgF64NVshWXSA6Pniq-sMQN^gZJp`pR70o5+V384byVVf3?m*#oV z38Ni#NGkPgaif+mtMoH*95R*@C6_08G3>`6YQuEE^{3pr zpToGD^XfOOmuu)UZ;1+koWw6l>Nfc%{zFzNF=@&)#VClqX`8*qJxFzHS;~}^nM?bU zYTJ6F%ek0}`l3@sB058QI-+w+eyl zxmE>|tLuenOmvvV7_llFFf%nkgC%uB1gB%GpLZIubJ{6p`&2iBw{jb!d=x@SlW6}5 zBL{_(tn-)9R-E$*s6_i@q62655ml5)H$Rz%spOG?>t0=(WolZWHK$ctOF&gyW`d@F ztQ<2S zWwmL(oFj8<7&ex(a9EX22W-C$=8ZLawYhq(ZsfUZ6>1%FqHPzV0mmQj6F-!%TOdp5^#c*V$O{ z*S+T&w_kdQWXw!F$(yH}xfNDoTWYjjOih5v#!}qIQm3<{1v&E@L&^3}bE>||Sej=u zPFK2f!c#VkyMSO;#NK4ZX&k}Qgj9qrlhB&kxxr#<4eGl%s?%BVJVwz_J>~PjH_&OykEL;kLyY(e7eUt zsT|C?O?7PpO8(5Yi>7Ft&DL42UZR#e_iH_t%!&E3f#5~RncrnGH{>9wmp2R!b{N%SFyx3am3g+=d4>k8e8Q_K2*D4 z;s`x^lw~FfZW^nw=*Xg39Bntcom^I4tm!HlgGj3c&0V%JO=>z;>`!DCNXB)?3|)DC zX1<7dG-Tvp;cLr1{6pw=N4!*BgQJ#IfmD!Oai4d8(>OPL_iRsA$;t|b&-qu~NNt5l zEcbjv;3{J{_9%Q)N-*uqk$aY!HnSKj(k+dRi2Y|V_qUDNc)E%r{G2e8+t;?Xa(`UJ zCGtmI{%k>fJ8erhc}FVKbydF%MQJbnF8pbE%(c?KyxadgTUAGvl>1|kt(z9r+~hp0 zQ1{Ag3r(7OYfyJqL@KZfbFD|zsrL-q7}eL8j4Epkyu=)05sYjY0+WTHyuW_2%dCF=UX!E z*y|{Pa^vBynzb@PwFS3sZ*8WpXG~6f-Og#mt)xWO%ok{w+9HH>6)s<~cH8n?sL;qc zN~xW(?Wkv)vB-ASbEV0L6{DVcD7HLdscf|aleF(;rhF|Y$@!>!G|^W<<9xT+nd622 z!R@{|zD)+kAj~w`8s&^S&EaEy-c(xVL*o@V$2eIb5bp`GD%5w+GEtFdYBB>No%Q)H?O!U zZCqN}I(n}oS;!Pjc?}M9qk^vp8yH|g$oTuVjZ~y+reCTjP8pi%R|Y_6Ce(X^#>;!X zUlSIo1E#B`wWUeB4V-!rwNRql<@@KRPRmJy&0`;WqOncn{869CTYo*?xe+S2fj+ok zg|t&{?nvz8)*G6>s&6`euwrYmzPozsEz1bOs>dFG8?9up+q-WDPkr39c!~bnWPI-s z3rYFaOxwsm$FoP!5xe#($(JtXa4yV+T$1j4x-KUxeOl=3&C{ReywFKkV^mt@i>*2P z%tkpEL|29e?6^Ey($NfFK8ooi`E4(5zXk{1WC`#VEH4nI%6HsLi&v+e+S)se?{lqV zlTF?nuI2+Cvp_6mpY7IV7)3TpqcAtph34tLwV4(|)D3Gs3WmB8JgWIc+&z_kmCjWi z?rFLC!S0+k?diy?g|yaHwqPz`9Cs-XwACkleo02!A$ipYmsNXDfO^xxdaI+d&8h4C z&2XEU&8?Un?cZQdb757kM-z31Ts{_*sR2sD>KLt{r{MWHx-DLq;r_hqzy3^Jxar5n zvaDA6-R^FeCf_x3l1*dc+q0b7dyR|^`;B~CeZ0UB&&u^ga*bi5D=n)q!K#TTUvtQM zAG3#vuF7(jWQ?g&X1%kKy1I4^YIOYfXN^P@s=?oy;k)bhGmX}`OZ#Md;7Px5i8|1= z8#nf#km5m!77=Qk$Z;V@9VjTUU_y{%#)T#$ zel$sOC2)>ZOX(M)umLJ zP_+^b=@Vg9hZ29T^m-NJ*RnZz4*gkCYR{@b&1MZ5HL2W_{tq?Me4BP}T$V4LMn!3r zu->(Zn_5*Yb)e9Me?jVOShpfUCND~AwOp9zQTkm+IAML*WG zJ5}vb(_d?|79Cn{WUx~`PL8Nn?B%Oa(~_N=b~N&=d0m3VIhb-+*Q#aHJRKM$=hxqb zYu8QnuVJ@4g)$Aj6*~IgPdz&vS(1DD=!m})k8PRo+^rge>$RyGJMXrMij&DI)IeJ+ zrr9WKt*irAbL^?=*78fk;L2(VDX+x4?m__jtBgVp-4e_{{&veQK!z-IkU$#A63s9P z$z!Xjpcu4GFx7%HYQX#|Q*5BjC@3vCxX??j$s$SqJMgzD^`ncxCt*}dz8iPT%{c_G zqwA)=#%gb}%94{##@}ocaZB2=>Vv*t8T zK@evn(Mbx8`b#8|tZY*^D!W7}H!^pusYXcYL{3rzg)39CPXUzcsiXA#P$M`wWA!}; zE%fV0KT+r|!RBT?&CNgs{*uVf=4%2QGtqsM}bWF3&JylR40Gb{v6pry`3xJ|sD^h`k z0N^-|p({CDbIUJX6A0js;4tanHnG@rIw=01^Yq0u2~#)#61pOxU%i|l`D79z)b&#< z>m^vuQ5Pf-Ea`}=5qg2n#HnfMSM$NMHbpy)zofaknY2+-#_$YqY_dJ$1L|&_S+~^h?>Qap7}I z^myq7QDCCdH9v>e?FhP6>1heZ5R8=1_MuY3uDLIlhu&%0|ZWV(T?f$N3fJ1LSiL8@a(L@k|ff;6S!f_}95CQPN>jz(R z?`^G5Ya*awip4l5Fu-Cu0N)Hm;6LciFLR9BpY`xJ0|BCFKMEp`;ih$`#0)P=@DWv? zNC%sup|Ce8IU3?jcN?6nFm8-Gl-4{2rYwzVRbc8-=|Y64^|Wgv4s_h#Nbt9}k-&cA zgWTil=eDugPY2U;n-08ow;2o$1#Zit{U~_1Lg9*8o(T^Mu)u)*q0Na7+?(~x2Z#N| zv2k!C%9~u+JBY#SZDtG~1JQ^#6;#oDZX4X&ytu!Ug)eGak=Brk#uOpeZbf0@NmU9~ z#^->EUZ^wB2zQbs;}uRm{y8}uWWZyb!yQU!3i8R^I)b0uY;Y(rW5_|kcsKc-PXr`j z9^AgRyj{ADm$~Gg4hAPV@Hq~dl%pIWbzlSxf=pWOf(#}FVgx0IZ+TeYrPsa}zAaL2 zZ1;oPyow{JOzJ02OmjjO8TUODfKOLYfJ5{g_qI3e&yd%9oBaByxZu&!S!9e78G%=; z6MB$oCN$BbT9-=mP%!~cbS!BIb>`SOQo@Mneso3qZl*dFG!w;KHG%xq&c z7AQ%;Dti9QTqg{t4uXp(!Y$7vNmE>s;!MQ4xov-3Ny5FFYi4=5PqRkc;h(`0#;zje zyXv;#b0M!-fZCXIGm1SZau8U5AE@o$faAl zBIQQ)EwWlW)!cLg=N;YE_lb)ws=ymrz;y(YR)4URKbj~k;{y;r?>>XPYn z_UPZKaQ6*vt_)@8SE6YexNU7;)wY(X;Fj>Qiu0_9+8ZlJ@i{v~{<7_``q7YP zm}XeE*^*-f<~9G&hK7yw+G-c%bnAPD>V4W`WF}_377s!21^3RpyK9EcdyEC5^-V@1 z;d;kex?_4VL6z3u>>7~5XI5-O1wL#0($+=D#xr|u3mkw0NZivEU{MDZs0Ty4;@iG1 zv#m*L2j|+>#jP%tsMg9X^Xx$tDsf|H$!l^a^tBV|>t?!QO0{*8#R`9pkPfpu6fZP` z`5E#G;a5_yUSwaUA4ceH*x`J*dym{z(EhvZ2b64utV z2@IyWv&c<4O~G@EGSE9F}p$aVnL%I=!GA>D+hLD{~F_ES#vyCgkXv;GY z45*NUq0^y36roH(=TGXDeX&j4jDjht(`)L_2M8myF z#{P??4!XmTV5#-tDea*iUm6%40K;npm?Uz;vih1Le5sdOs(AhaL&hlxLev;dW3l!+ zm}?}Nvf4uiB1bGhf%O@iAH;$Q^r@HPDR%po^O%(<`4tR_jbDMabbKpE(J!mnW;T@*rN zdZ~Ntzp?tonB=A#(hio4MI+Q6jo~MkqNDyBh${3&?dirZw8bN$DWCK|BV?(bN~PDc zF)ScMo;)HnoW){v!=8di2`sCqETh(P4~bKg%cDKg6S`8HkW}$FQesBvq9jz=K)9&9 zWJ8l)fi^y4!Neh{(8Kxi~agz%{_Y{bXB7juk7MTE^<+?hulkAAVmLCnoC zESU#1Le4y*w*wg5j6uUon(#8Iu@jTMBeI$)z^Y-s*8@J)^1wM+tE#h;TsbyW{K&yD zz^1z+x9Xfnxh>Q)u`%p%-j4fdR`lA}+vTpeK(k^qa2 zSiBP9DNOMo(8~Hb+VdRO;k?t)KN2)Zgt`<*OdPK`8qjGGbOFssvbRx5FpC1aeq2St z#5?8-JHpU0z{E9-kr4r{;NtXPrYNb|Ioa%a7@44L zH5H*t)*=}0{5=MfQo-^(rZ^cMB#1AJ%+C~v^MlG;#6vkOh@!+tU?hmld`yBE%4bBv zqeD}J08VzCn2GT}ec{c-q=|(2!7!9R%G`*}R2T?+ONXPr)Z{^Zi43OH%t`gklUW~w zXiUmvK#U2iumPCy!N>blN31F!S?LdZ$jt4y;uBG8T^|`#N08&Q#o~{tvVDH?;Af4EjX83R_yzg0>LzG6|;>SFOCc-4>i7p z1j)I(#0~vb>0lqyq$vh^17CcqnEXXv`of2(rec|a;-(LHKVqT0I*D`OWNO))OPP;HeH z+!D5KE6*TpJZg2zOrwxkoXpQ`%9bgsc)G@eQP%+INjpU#A#}rV^-Uhs8xgRW0N_>~ zd=&1xpBdn%fUU=YWx{3LSp4%;;}t?ottSL(TA1283oMQ_WFK^-Lg;nFu!5p%jEH0O zLNDBN7@?$hT|I%)8KWV^F6fOVp6o zOp`j3wKQLwH|caX;pwpWyRQXW+xxt?2?jCf5)k|&RCUqRqF~RpO$fzZ+aO&GgUmLB zn80*fAk>)J)51+c1^(1>tc(i2nz8FB%6(S*&DoTzKdaHwi<~fc1 z!_sO*?8KSAnB(ZO(loAFXvMFHDJ(PHKCUye)}fSf$w-Cs5E5Mx2^FOp3&lvp6dvVB zo9({IJ6Yrmogh0%EzUT(q}&skB~rVXk|C|INfU@ntzymOMWZe~7NR31JmnnOdjq@3 zG9^2%Rsk$H9&xUwL%s~|yx!WxjB*GgrWptwk;FT|9A-gI0p{_;nrp?k1{a7zyrKB52V>(J5wAeEY=kpWG4RhRxW4TI8j+D?C&-}&N{bnk36U}SOXBIm{ zc17%}M3|9f!;&+GLqx%A<7j=kudqu?b|{%)S-tEqHZ!syn%~igzHGI~UWK#BdnoTj z*@Ny#Nu%F^>*5+2$|y9%ndU;VGNy8FH8F|{850^26By%24hJz0`6HEdEuJCRUUZAKh74Q{$SPN2rsY{jH(*}mym7>A1x!=5#?BW_fL zwBWK~3v>wxeBB-yND8B7A2zt_klCpsv6op15u=f(i@oOLOBB?ZNCcWc6AuwlVo-Q#}wkd)dt zol{T)*ELBRETV=vQ%poOEztS~9aA$M9WiQ0@4ClT&VBRPxT*%rcHX-ZW9lmwhD=$j#az8`ntBbmzL+l$vFKqsG zpIh5R!Ms3v2DB6sayJca7STIJU+ywyv!I43Ft$U3?_Uy1*ocyBM#J)C2S^+b zy@z8CoE!*tun_Kkkx?Fi%V(5!y z3_e{nXX%P;-`87d$<^*_O}xyL7faFb-LIGK?s3 zBE^aVAzFNxF(bx}8#_h>$kqCYer(tL#!?S_8JH>SqUGC7kzDK~X!*DpndKfoBr>1PByIO zwL(Cs56kw49kK4=Qx@x$t^ND>2VU6EzrX+g00t=FfCLt3;DHDxsNjMOHt67k5Jo8B zgcMe2;e{9mh=W-3b!cCDjMYb0d-l;cmVEYQW*&)3@RwPJFvck3j5OA0UE@Qnb%3m|Z1UnDFT$suKwy`WfSl|?}Z9S``xq*(hY@mPxjz6jt5E*1zTk7Sl< z=9y@wspgvf2{ERBC-m1L4v83I2pWb&0@jdQ7^#E{Xn+9UlS^*lB$I4yfd&j#;E{-Z zitYEq1tUQ8A6I(Np}_?w2+#VfaYU_?T zyt$tc3%z;e2`~@+o z4BwiOr~6eplBYiqv=F*zpz%Q@^cnhss7k_Xb_4EInzA}54mMIt!Vi-yp8KM+MC{u&bSqVy(`D%YTgF8b&V2kv5T{K>5Z2(r{*NDX*I zGT#d<^vx0sw%-ks?jR9xL+%^yhl6_R-3$YzTxJR2SDCfwDVSPPIw|s`S8x6G*y{+f ze_@R~rrZTRz$n@*5cC7v6`_1pU^-DGt;_=^pZ)yw*KhyA3(bva{z_CU_g`>X-%jAd z5HJbhfCs!l0zJh*1v;>S5PYBnBS^ssTCjo`yr2d%$iWSIu!A7{pa?@q!V#LVgeW|r z3RB3!6}qs6Fnpm5W0=88C8jQlq1_9@b~hXrWFY4!*LsoK<)rx}0 zFv-bIKGKtWyksOr$;nHm0Frg2Rw}jN0~WA?7K@0bEN4l}TH5lKxXh(4cgf3M`tp~+ z45l!LNz7sz^O(p?rZShw%w{_CnaE_t3z$HQOf15h*o?(Bwb==6dUFxoT!c5f8O}_U z^P1vJ=Qr1>&UUi%o$ib$Jm)FSOwjY5^8U;xKG$haefsm8*4(E%1KLh(Zu6h{%qBVo z%20$hRGS8cs6-FS&WJA5q46YWKOc(GhiJ2k0LaXQqZ67{G?O)64}8q1|B^{Gycs!pLwRjE3a zs#SezR;{|#u3nX=VD+k4bIR4Sj#ZXsP3u{i8dkN6^{hd~CKIASO|`nUt!vGzUG>V> zz52DUfc>jr154P!8n&>AJuFuf>s4B~Aqs#9Dq)i7=9JKgF+m${i8u4uR0RGQMYnyDqLa+{0V!`2qFw-qmS!#iH| z&X&Epm9A^cOW(hqcdyU2ZGHEv-}JgSzH()O8`#_5$@&++2YzpXgS%k(Hu%B(Wo(4i zn_vi=cd!7iDlM2WP4I&Cu2{7&fhSC25u3Qg239bNx%<@>qZY9=WkF;s9Ae*M_qH|O zt&2A+-V^(H$IH#|kM#@W*V>oG*W_?(d+c5ckNCn(p0AFL9N+&kSj8|`)kna^atGmVej z-ZmRr%7xCYo)4{OG&|bQH%7Fejr!ODuX)nV1@vMw!RR7O+RdE~^=UW4-+*^e&qvv)n~c78j*#SU%_&voDIrug0A{b^7y-N70sddhvC z_*N(Q;fo*qo4?I;ZbzQ#Z`XI&vtDeigB;Ct?PcfR)F8U15UEsuLJE4dA*w!Z9?*GPp*{@#sHJ6_A z#lJV?dH%oq_io?&W{)<{JKu5XQyKKOPc)tzTW!F19`R~#IO>J1eEHv=<{-|$=|8S_ z`mcZXu*YM4mwMPIfE&hI#)f~|Red-&ZUv}-AeM55r+PCtZTHuI6(?~hSAWTeeHfTz z*7kC>mv9`oe`EH1^LKydH+jhSY$LdJboYVK7kdFXen{7X00>|Qh;cKhf+$#W-?xH& z)_^kzfj3BfB`96`CvHI~dHx50vqp9ur+JkZVH0PBuP1FNNON(Pg26Y0+-Gk!7=AyO zbT3zah34nv$cHY-ggyv{ z{&g5t-o=Cc*L^zIhs+j+N*8*k#)c)>ftW{oEmnw!cZre5aDXUxWr$4&2zbJ`d~x_$ z8>otLNPHHEg^Krpun2`}D1+H&iJtd~I8|*!M~j_?i$+IqYbbb07=NKCgo)>iwup(h z2z-TX zac204%?OPB7Kna0j`)a#_9%NI$bF)icmHU81Q>r8xKwk+h*`L9mRE)R_>E7fc;zT} zU+8~32W8s0kJnd>3RsB+X@jGvT#glj35kwuc!W6kkmopy(<3#bRMXY-=~Z7 zxN%Wgg$UJkOismhLS0n;Am2oxtOqbkMBs9NQZunmV2e;hNX#Ovgnxk=!`5Gk*G;j91sS8 zU`@UGn?pqlU0?=_5CbvLbD01L9Pk86l?gyl35;M>sjvt<-~e2CO*#G$2HY72OJG(U zPzi3pmOA;D2}pj~28s0*k~wL2Qu&dMiIP|OlV)%MWuO8;Z~+rwQj&R1fbbZ!5Eih+ zY5jSbM`cieZ~>&im7HdoDi8)2@B}JQpq3Sy4v?2&r2-t#ANy%dDnM40>7iY40gN!3 z9eJBH>63`4jKWxa8}?oBd3r9nmR=d3?pTJCSXBQx0&Xw?Bx;$Aumm|EPJl216Y!s; zfKEZ01Dpm3B>-L}a80z31Hu^#ZlIq+1q46<2yRdUnE;`3Py$OZo=8OtSzw*1ke!)O z0#7h0VPFdO%2($oEncxOXU0 z7XSn*P@g~mQCHfbivS3qN-1ta3mZB;7r+5*l?psyO~k6Enc$;tpaP4)0T6QnfEos4 zLJEwa0wbCOrT{StfQ&cVlS%2GCpMVBw_1jogEktIk1CeC>4eH?pjB0*`!SudPzm;F z3iX+sLU5#~dYl(<3LN00W)Pr1Kntqc2#fFoLSRz2+Hxa$3Qqv43A>>ez@45lou}qqM*X z4n(gNYnguPxv`L-9h$5W8>@C}3KyWY3*`V=u(oYryBn~19-9fEI-U<2p{nYou>he@ zpt4`OAF}&+87P<}xL({ksft;%>!^r1%abwwi+9hPy?kkTv4~VEAP2F_X#pw(Oqv5g zFse?w2tq&#fPk=i3ab+UOQe9ZZr~~LYfx883U1&AfZ(G*un4d_0>0X*G)1o*z^vk{ z21KyH47|XpKvZs9p8@Lz^IHO&I>A1g19=MwK#;it%#r~Ky}4PE0vNrLDt$vGn-B?$ ziu;^qqp?a0C@Zjw0KO=@A3$JF3J|+@JF#4OrTn3#lCmqb z0HF#5Ch==hsXzn@+`tR025f*+yGy}Ad;u|#y7M~%o)QC@Frth=uaknSGa9ZjinxZ# zltdMh<;s92jJ-k&uHXl$rMY0SzzF^^@UrL>ohNV#a=qPzi8) zO(hTmUYY|jFrFp_2*}w5U62E@AP10K24;|)rqBi9IRP;soYq7O8-T@FoU!Os2CHmO zoNT$y*`zUGxg2m!#p?z+&8F%8OQ_M?xQJQJvh|^+ ziO8pUQl_g?E$7V!HKE+xRGEOl6gpH@49g76w%No}2W6SD)Suk^P423FB^=FoJcQrY zfX58Dq{xf=Oo#7RlyG>9r@ zn7WsJ43*Y=uHDLY>d1d0Z2rAI3(fYtz0&KqUWmdt#ZBInw&8@&G6}<7IMU`R$0>?! zlp2l7?5*>t(~YODf;-LJ7)zZnOa8pLEKOnuy}~}ov%}e_b?kT?narI?vx>@z>KRZ= zdB-5_mtPssdYNlgR=t$ky#@`_lO}95TGEHxgl{aL$lSc_D2a!v(-Fwif>pQ0o`pr_Wf;n)NSl>o{m;N^h82#dg-QhEv{%HrS^3ua&nbZSjk9t%s* z%Wl93tV|33dP-BK;0D?R2&V9xcdDn9%m{K226{fFL%N)UKA?yk>5et%+Vr+DK&4+E zr9r3XfB*=v#HAumQ?Ud~-Ic3bssaii2aHh6hz#a*{-m+Q2qiFMj9{@Ppy@Qr&u0V5C)Y{0%jlw#oGl6V4N`!24*0vemVhi zK(`#w1(jR|rm(binge_rozM>Ga$5-skeu1R?Lbhxl>qH8{_aZ<2B!e+e~!wZe9Ga@ z!Hp0G6VRKCFaZkC>{JfzT_BlHAO|Y&1VGRQ_1*yRy2_Q1^{AZnb}k3r z6a&Nw2r7{8)}-Zk`vhNK30H5)DgemZ!~qJh2xTCvoNNYD9|H+b_5t7aVbJ$ou%@J3 zt(7cJ53iw1&;@J{1d&VeIWPh^TnQ5(1ZEJw1kVT~fb$_M1TSs|(OC&C9tMqo^=1$T ze$T3zknhZj%3(13PjH`SyaWZ$0jDtWUh31mesZ*}RO%?*DM{9d3Xs>^&)0}lG5+wK z-)s5@eCK7L^=9y!F(Bnmn#+rD{arBVOAw`Iumo}-$Z`t^7XQ6rkhW%kj{SdVV1;jqhd*! zSeYs*0Q5u7g)#~_l=(pm<)TU#o~ST$p;4?w3x9Cn@WV|R4uBY$3L{5_n=Y9$ZB(%+ z1_uR68_7|)sL_&2fZRM;RHAC8nq9ee1^ZR(BCln+o-G^J>{_&K)v|@_R&HFnbM4l} zd)MtECQ+Es(z`bA;J{-I6DC_2@!`3P9ixq#2~Z<5W*Px9^%BUFGD|oE{)%M=P{XBX z&CoK{B}|tbX1bVKvV>+P(TqM`Hd>}cEZt?y#CWnfMx`*7N^lLcG|Z{ujEJtz!ANx% zKtQ^bImIE2DFvC_fQ+G_QPD+!DwU~3s}k|ci(R^eI8_-PPFZ4|UELYQ68!$cGGdyCuGwOuQEqr6~P|(>%fG4dg`}zZ5r;d ztHxSnsrNqm=Bt(Q~(5?T!=#n z@z0Y?!YLYZN;M@>fEmtYhA<=qCt+BG9AaPw?F5I3U0_6asCTv4Wlw{KgB|-;=)Mnn zQBY<(VfWNXMi}CZi}E|4880R_(Y4AeW^hBNq$mgUJi#I=P{45%P=@N1;RzKvUJM>W z#Q;zx267l=b$}2HMsR31Q(VLoVvt2zsGtO=fC>;eu(CN$ZCB=VAswwJM={p!g}bs5 zj!qdyRt{~8Bx_+UGnh)YK(PdJ7>z~n{-m3CnLz=-p#>0l$bm{ci8E#}MRo>AML9en znWo5uCKJ*bJ{s~T4Kc+NbHIT$YHXLf6Jt5qxJnQ*u9W8_VJ2#I!Y}4ceRrE33%B)3 zTBG=LR}s;FZ_}zT;tgOPcpJqtS*J8i4wOscFxsxEk&!={7O}@wsWaTMJEUI znoo`*cBYL*p<(qJ*T1p|jX_Jy^#aRSv1(SGd2JtHO^dP2n)9lo`zk6G+g9+6t+pyf zs$ExwQu5KYtg0QYW<9rBeEu`3CbcY0fty&Owsx+iMIUp=w%N#{HnxHS#9C8}x5qlS zwsjQYWc8U_tcDP7MXl=^L)zJZJ{P=Zb*)*^n^qHo_P6PM?Q%ssU5K&wx_jNHe(`%> zsGioh|8?zgzxUF`E~W&sq^eZ6DqZ!C*SwewZhnQj-ST2MrUM3XF59SJ%!N3#-UV@3 z)Jjk5!f~Th{ji8j319xQ;ugIKt}%g2Y+(=c*sq0cuXty?+Z**3xkK(QXJxF)`M&m^ zvNhf zeam_}*3USOGG?VbXiZ02z%x|D5T{{+G(0_A#mXTg%R+y2_oV z-l$V8HMJU0(haKr>Z?wtM#x<`Gok3VDmn{$0bEt#u;*|y! z$vVz8bgPZ%^8Qk`*{`H4NN2t5Ju6zuy_VXKRt#on!w>EgWS zgqF5S>$~5KQ~2k?6}Y$i`)HU~IN@byw6X(US30NN!clLF&_!)rucMu>lGXXH7Y=u# zySd&kKdZd8ePA)W*R3gj)YkpYZnwW0-GsMwKoQP$|5Efb)|x~RY?&-;DwO*VYs6JPkn zNB;4ZZ+zw_zxm5|e({Ymed<@=`qz&>^tJzd?qA>g-v@vAtDpQZ*S#z&qxb7a-uJ4@ zQ@gFV``%rAlo55I687&x{>RgW@!TK(_Xj}y6TkorzybWf{PRBoG{6J&zeY$v1cbl> zEIb!YZ`> z!YjnWEY!j+5FE&Pvwu>?x^gfm3LG*rViWWzRe!%wh;89Xn`i>w_7b-5RfqR!#@PXKorD5B*a3@!tSsdN>Ic_WW+`sghhPBMx=yDl*CG8#53%KBYP{< z(?ghg6hIiC(n~Hn7{LL4Xr z7NLb>{KazY#d8csUo^vFJjZhU#Xy)vj1$EMTe;Lq z2!wFl!(TI>!JW{fd_y>7I*+9XhJLSfuQsO5cmNruu6mk%dl+4Q#b@L>x5vugoqr3 zkMsmZ&;&)G1WkBLjZ}m|&;&k!$VqqwKJWx!1O-HhNKgPtO5jUH_ybR%$di=DE(5!I zDkatdt=TKIq!TFlTMHG4Ne@th7r=uH&;T@$%1{u32lxONxPTgXf&LRngeFXc6HtOg zV1XKdflx>UD?k7%sDU4F0S~x=8W@2@aDfgm1Sg~e7l468_<;|&0T-YH9B>2zfCNLh z0W=WH>aYw4{Vd5Cu?pg+JhfRu}}qd<8`KOHDuoOlSpHm;_R2g-hs5 zN&p2>=z~jWg*L@fg7NMQ1F37 z_)Qo z1J<;H9d*$Q@PMbh&LJgIEzC~27)M~dgu2WFKCsJ6D9=H_{>x36gh6-(OvnR{qy$fR zg+cIyEcHl2Pz3qR14Qry`uv1fh)6Re(7mHN%6z;r%g2d(yho$DsS`W0z|1EM1vJ2c zQ4moVK!6`WfE{IlQMl1i=*cF;14y_4IuOwdg@LTRfd@bXo>WolyZ}EH0wt({pnL&M z{Qw_`gbRQH5v2nmEz(tGRVY+Qvqa0fYz6aF1X1t=R~UpQeFeC5g;sclGn|Ak_0lZ; zPFGL_z|;gxcuP^xgtW}XHVrb;bDC=-DH_bKiNjN+leu+?$qRS@oQ#B;!~p`h00I!o zQfU3PJQS&g zpxer|+=2uHL72fiaL9JNMXs&IMXX4|HOKy8tVLTC1Sp}ldwf&VV>(*FL-Tsw$bB}) zZ69>G+}zb&R_p>lr~|e*%QqC>;T>K|;Dl0$$F-HlX>2aZEsG+=)-=;CqTz}{5CAX` zgY4Db?d9I?_1^CV-|!XR@g?8#HQ)0^-}F`A^<`i3H2^)!#kftP_k=G6ZnMuHYTk zVIIceALd~orr{&jVTpZ7>7CZXK>k}`LAK61tI<=8Sg_(N#^Nm2;w|RlF81Ot2IDXm z<1r@VGB)EgM&mU8V)uAqo3UaxR^u{G!gyo0rSp;OTF?h>&^M&v|R@S#~^++EyWhJ+_cIIiy*VRy8=~=$7_qjfPxD z+gW>5INF++T2Z+7IV+mY(U=nz{lDus+Ub#jEFPWZjz9X`goLpN49_yX69H zU2)zQeI8~z8^yxv$A_N7hF0jX2J5k2=&}}TvnFeV<~cpyJWPJ-!0R3p!oBh{P-14g zsK)EJX3V;FuFBKoUH%bptL0EOXVvL0y+-W3ZnVWrTxfo`ueLJr`Mf&Z)|e*6c2?}f z)@++*;%c5~Zg%Vbze&7S!xE(qYHjvs!p`i~-t1c2M1W?{c?|98RVUb`)7gEpkGkwR zv$nJL?X(7Nv=;8)9`4{)?PZ+azsBg9{@7xc}WSrwC-MJ ziGEyuyzf-&2$yjT1iy>q4VU7?NHwJ>=bD0u{5H_xk19y)_c!(uX zfj5o_1sDGDeva>je(iMLuJqRK!03ydO=-m~Y^$c|0>6^i00a&If=6NmO5%XsU=Sl& zf(nuYIardVc%&upj^Ox;G*6Q?XM`sJgyZ>=GN1*|D4_W$fhQo4K)<1mxC-saq$OyR z1VWT2kOLc$a|D8A$5uVM7H}AIFw&+>p+;?ocU)x(I<7B|wM)Q)q}iVIn-@ zi6?+2IY^BssE}&;=AN!}#C@Y;Q*K|Q^rQQ9U1^OYr-c$|3LFB00PvJ0XryXlCJrzN zBK{ZjGFSvX;dObZi6_dF5+J9N$a7sM2trZ-AmDXi4}cO7^%5w90Qin#=K%Z34m~LY z6(Ds`lJ_J3=vbEWVypg8d` z2nwU9_crmPlNfa0xFP<*ilF!nQM!mH_zyxlaXg|TTIY(U4}*z7h)n1LH!uZAIte^s ziaQsT5fA1magH*%_lEkCcmd=hp^3rQ^Qd;W{yxKP zsHtDRTLFeO!IZNNTiau=c7yo}w z<)-#!+Gf3!KhSXBZvqqf>X%kd$MOrmYz9Nz+U0iSuG6_&^0WJ~uLp=sixvqiNbsOR zga;KWOek=nL5B?)MjZIC;>C#=Cu-!laU(=pfTA#=^|2$!lpP^jWSLRo$(SBjzC>wL z-d#?Kvm5|QPjwP#S$88_h z>-wI;_ts=DyCqm!Zpqije?LBevDu|laAM6@)`6=Hl!{4IHDnZo2O*WoQcE2d))IhV zMHPicRrsNU6cPl8dM=e<%7hzIxFU-oD)mHyKN*PHjJpAs(T)DYnZ^W34JsHQkYgQa z7kmQxR-k`I3W?)K0x@V^k|I8&T_(p(6w4Be)dYx0U2;j@W(v`=#DoZyz(|-Ci3A9n zDUqIh#)w;@i#Gb3oHH`mi9xhj zdMT!va!6Dv8a9+kh7o1L$OI#dAcU!=qB;R17y$wVMw#T$s!M>JGQue_==#GCWqg$7 zk%~f!XM8dW7$>sABB*RZBE4lSvwI$TXS9yWX)TfN9rWZyS*SsT8sCNsuDIbg0Mvyp z!D&z|W?UcyDL;@BZ@fQT5QA13jagGHv!+QC`)?*BmU+;bJgLBe8QU!e$OM28y$G-#YfG`T0cL&m$r&Sxb;K!Sq#q0X z9jmCrC|j*|*TJs66@wdk+d!CiyPd|mu7ojMfTENXnsTqJuzs5Kr%#xn zRZFx)%~QavVOwL{A&Y%E*IF04h)9I;m2Hh^N9w42j}Cb{=3p;K3#As-+zCTrddo(d z={giDL_RZlcnoC&>@>x+8?;!dbbCqRzmF>Ard@5(~yTAn}gs-yN#qm4Y)&y z-|oV@o?Sq35%FI`NVE|G-p_GT!eG>hmWk($?|eoiAF-ylBqTxaVJRsA3%sYA5;Bp3 z#roQ`K$baZfe=uWy2u8Ia5wARE+h8C3ImBEIQ|(#1vreztBQ9KS8Ag(v)rO4QF&ySN=}9KUKy;Xa~%Q4n?&H9u_1P2F%y;q(+f6HZhh|bMXUJmCkGsKhBw@zhiNK!Fz_=Sy2kOM05H zogy2i_N>A^vsH|veH7^{(~8!OiqNF$WM}!d6ou6JGGwl=vt z1rp?wpeu+qiT0|n98XjNyTAo10fZ4iR$-r_TF2bURDMoq#bjVf^04<;a7GYA)9MAg|*D+1A_JoW-% zpam%^YgLP2b&8tBqCB&D-L<+kw3Tv8Nq=dhMIw>Bt3B^%-@4VQaBCs%drl{mGsaKF zP$4Qn=%^C*E+Lpf2>zA*fhi>7P@m2!EKn1|st`PaGW4WstTihIZ5clE_VS~kZEZsV zVVl=_RFD+zZW24JED(oQWu1uKm2_JaIkF@c2BlCMG1w+RxM9Yb1z=c8*^xHYm`x`E z9En4UH6?yHyW6#(X9WV@8@rZ~TwN=Yi~Qu)iPp1n?Mv}eVk zrxArp2#I6uAR1_jY(|wtaph^(n`S=ihLBAjkW}>3y#{HzWPjAI63w~NI!EwNH(41i z+TZ{OfNn9&{^7wQ?7E0^)f$GEYzRVg&;=mSq6DA%9Bp`&#uCI0 zF;D;p++-tv7Oa{s-$zrc-c=iNB;hq*5z~#&R>Paa!i23&xJ3&G z#DWLLk#!q*AnRE>z~8ejbU_V`kTk}k41g#D2d*84GCU#TF_1$SH1lXOJyfaJF@pli zaP1{1APi6FLKrwO@uo!t(dkw5Nz00Ab$8I-AZyW?SYelgq>1uS zR8Jk+bm0Z-#mb3H4HLl#&qZJS$pyX*p8L5OvxwTe%~SK)8T1)ZoEaYa7{_xYM|A!` zhY6CPawLawbRI!qhj&bgd%+-gfCo%9LBx)coro@Or;dXH#Fv=2F=#t<$lKv8kVlN?H z9$Hxil3Ogg;e|P06VhBgp_HisUs!z*N@Nh7y0cg_(dBrM`&h}5Dv24#hT{+LysSSQH-(85BCa&R68e#vrr6=MWH^wB_tl6E^WL=7*UOuB{F5zSj zre3_{W8$QmA?1A#9zxz*WlE+h!emyansu?JVSeV#!3JHrCX#)nVoD`!%H%szWC*Qg zRRZT;>Y_U4B|S#sH5TV<4yV;5Az><~CE{Ugiso@n$ zV@@{8auR57KBx8N=0QH>dnPDTt|A=5r-WRch)h82xxwg-$2JLRh5*46zy)L!+Y^Kb zlE%Pd070@n0g6__5?JZ7-5@PE2J8jcLAVQlxIr0A&3aOgUD^{$9-n32p%B()ObANg z#VG!5q)IAdXR_IBCMC`dnQBo*7?7wO6bvoI03(oPvJ|LF|>lnA)b3ndfG5XC&@u zMEa&B=3G~Hlm#AWE9xdVCgcQSO)U86B6IK*=b`+cB3xh>NaXrSk~z{ z0z#bTnN)`7elDbNnkKN?X_@_LzsP|K0KyZyIO`(=S4p{1VkY3Z! z0>qg@$VKcT$UzRs0g6IK8FYc8hQUEVYo-c>n38Fl^5|x!pRK}ZM*eHQ>c}D0--i+G ztVW@*>g$aj1?U|`qs4-jbkkx4A}tKX)9#==LaPFpfwwgY>H#c*;w*pO--B}Po;ng~ z7OY2FquFZbY?>)^3ZtU!#auv%irfTQdHw{KY-f(fVbFH1MeZ%1DN^zgC4s`K)`H_S z&MVOhsAK3|gg z5?T%|-l8Qn4lJL9&fJD%uX@$yrenPV>()?ipBC-s0&MDH7~-xj>6Qk1UY~?j?u+(n ze{NOZ=IQPFkvHxkX5Oss4lng?&W!de??xur_NRIpiyTtr;{N3nfo82XVV`*~?H(@j z7GDXFgpQJJ>6)+PuBP6qrSkHo{08eeer@`m@AD2JXUgvI@GIb&Vq13SEO}<;s;+fT z@9{ov@9wWt`sV_FuVPNHR3u+l{(k4qmg(9uZed2|B&MZVx}{ONrw1FS9WJP2uCQNR zZaqToUWTpbzSae!umj)I;qBv!^6v--aaV!o;3de|itqLc?jyD?s=2V;(y($?nT_i2 zM#qYjDr5(x#eHu%1pN1LG(OGh`LxZW5DFbK0RoCK`>_=1OYt>6RxL zL-BPEuoD|D4CnEuRp1^cFP#SR4+|^oCZ~fwdxi>oVu?5Lh#5J-`V}%=>OKPn(s6#Fg=GbE)#S0VlYYi?`_VoXXf8k zg0DWya%sS6Ew3^-%VX%OE-yndUsiH8YbPCh@Gb*#F&}1Xelk2^t{~rYKK7k+>T)?3 zat`D1@O8BP`tm*-tk1e}=Vpx?Jj)+nax1U%KxQ;S$L}d)?+dGMS>~^3<}^ZsGVgBm zKO!az-z$K^vO06GB*y6ag)MY?^t@5EQ$wOW8!^AeGgbrS&MGxd^K=`xGydvpM~k&S zfA3j0v=R#JOqc#%3v*TTEpob@-KW3fxuC_gv1e@pK&Q})lw z(voqvJN|DPAzyQN6EQF|_+R_nWJ|bu8}l$*_Z9#4`*L+;Q<-d1GCWrAhtGF5iZF2F zb3(g#hA;N=HcK22aU0M0|LVAb2TR8Qc3S3kj5ly>w6wit^S~m^FEPT2kl!A$Bd89l-A!Ir#2m(^cMZ`mGcDEUW@5^tvEq1;C(s6O*{2uQgp{B5Xo4pAI==7q?9`yDId$ zB+SArNW!?U`YM#VNk{^wOG2=(dLi&S0|3k{Ji;u@dL}%A3ar8-FaRn%f+YC+y#u?g zv+!-^x)Jz$9R$3$=w7ANIP|e%9}~Zy;7(oeU>Wa4#IyUDDsU+c{BO?}{JF zkp5sj@xUJc$T%O;4(w-_4k&NEe=>}|lz8Z_5V7EYEtq!;IAo59`W?0*O!|~=&W3UB zbrL=;_tPD(@%s^JV&zE^?4U=+_&ZKkC-|HG_7~!_QvI*?{L96~hbkV29vpNPLrKy@ z+tWIF`6U;UI%{{)0b}A1D2qSJ#GN%#Ph#-S+G&|y2mdhOUR1UMm*=m---3`cYZz_uu{xF+tUFT}@QV9ok`{$1BU zUIN>j?hi5)RsASTjE9el%VVF{lspVze}=26v;PL`kTLL%(w|OUvbeEQ|LA_afK(b`NDfa%$<(V`jXhJIsg@xEDi&wh&Ay7I z~Aae4gle=4py%jxVx)hcir)fF8(gZlT=N~&?*Cx-RpfuUU= zQZluQT=A;us?2q?d(Bou5dm%v!}WHX=qn=;*0ljo0%d;zWm3I%=XLg}{5Hfvr72hI z6=xO&8HKZq>5xS3PwU0}H*wxz{WsoxMJ)!bd4l+N}^z1h(3+TNdQUCN~EdAK_{3rcgTrhj{TzhAR?D6t`k zPNOuuEO8Oq`wN^w?XIPrcLNfPlKPf#C6dJw!a0y{S9sFv-eEDRB=r_6i6=+`jq+wV zac0MpdwzE~5WOQ(_~b16%>9iN-<6ygU-+6x;;)`aPGqF=}$U3DDYM*LrEK4y?cxdX%p*5d1 z8@w6xT%g$IY|)w2u5>Jk)>e;bD``lO%lIT&%t~RXQhZd1#n1MhSld-IgfZ*=_ZG%b zbvI#^$j@C(iE<~)3&kEO4J|C8g!Miou3;H)z>OUcv<}Tp{QM{tLKpFqWe&k z=fV!i@OM-5%czZNp+2E2`WOP@``ZSlHo-J9xw1qg#J$;f=PdKYkhc zDfL@fJBwa~eKuq4esZS$Muub4Zh3@p+o3hV%l485{FiaIb~Hs@(8AIQUQrhvMV$wj zMK1}@zmK6@j|~(mu*zJDUuB^v@h9nsv8f4UZdh3ecMwX;>)0ue&Gk!S`p-eJ8^wa3 zvG$&nZ#Ez6SQj6Qpcuk_m-+owajj!jr~{4x{sbqV@RxJS# zBS1++W{fXe@f*ToE`2`C>;E8*feS@)+6(WNyu^$gP97{a)tOI0Hvz_&G*gsj*i;EK z$*U+yPRxDmi!*k1=zaalUiDmpxwgD0hi6C?qWfRzLp+$`H~|WSe-*hss*v|`Anu|0 zvQ~X>%BGn*^ZMsjdG867Om)Z%Vg`kq-H-+eq?gpUkQbqd{Pd843Ul2%-o`l_13 zjAnq~C_iv6TSziT zdgOmvw)Io1xw>!4O(eW@=cj^c5A!b!_FGnqFus$|(oDt4wz&__F*{b)s@~r7#tS#R zF78yd?bhT95wG3pka@;j=&tpdv0jc$Xy-ti$2idP$EvS)|H`bJpVGiNmXmyc`+oLI zRLtd-wip`c6@GM(PZJ@VfUbJ3eM&)FRgq++y2Sc9Jc}YYYI66?FzDB0 zi|RZ|`gZbVG(V~BmTe>5S&Xr*djC~ahutMYZC*M?2XFI^TcTfSAw7|mZLmQtT;o28 zwtWqyBy9JXnz-;Tr_wm6%vQ=YWm(UxPyUkS*wrLmm&f`#nl$j)i-@?{sIiGUo%JI! z$}IoMD-e@$Da%LE2mNQNpsHegJ7+QfntktIUy~gMeH{o&)M7q-JinW@#<3c1wqxd3 z^(>MGt`3G{zJEMIQ|WV7sGcloMp~qDqsE^@=2ad!S{r@XHbUQ=rdZi`?CnR`;l6a)_wQSEJj;@tT*A z-8cuXZNoX)H4z2gdUdYpI}zEO98!kIK{h?_?{l8N&AsUMISTG(OLo??K4sed3s_Mh z4ehsnKF-=7<~mB=#*Oh5OtLKyLmeilb|(}5=w0(0n8S8)p%gm-r8*SR6MZ>NXmJFn zvHCyU%!PY!mg6C)JNaf!)h6=}=z2Rm`z+2p75-_&m_DNZsZEgvJRZ#Uw|oBnQaeP2 zyZCKCujTaj6;kWpuO^lj-aP_P zuM?P-YF_zwFOes*FsB45!&rJQ{kZede-%E>*b7RIN7!9i<{4kti82+_P3?ap%CzUc zC@#H*OyBK<|114?5X|-CmCN#S!YmJS9xf3vT2`D`{UW&*yEUo0@|Onc*XQ$(Tj<0s z_33rMuYbfY*o<#*ws%!dMlj(Y_eqg8%VP$H$$0?cGeoV1lIx#?X^cSi$DvwT^mI`) zawR?b+fnW6-=7#ZGZ~S4?Q6dXYep{2>;4KvVlqXz3fW^aITO&m0gfjBI*MJxodJ+M z0d_v-yhqxq#4jfj&3s_O&UE0(gcoH3HJFQu?k{QzwbtWTu=Y= zlKztS#g*HOyZRUZE?;2sG2pl}5Hv7=e>0Hqy(A~42TRk<&C;5^#2jKEY++!)_}>Xs z?^yfXu<-gqS%~SiN0JNsLLo22!k1l(-$#%z3Vj11T?j#Z>P$|?q{|-!*$c-E#1AS|ku|InCZ3_@rU+rkh`NF_rs=+*6J2tMR? zE0jp=Xe$)Ug4wq)oK)?$D2m!y#4MU3w$dzwmamc_jw)CGWi$=eNO2tV(DKVf z>QwyQc#Z?sy%trm4{9n5RgzWbY-&h!I$&Yn$WmCn%g8z2XVZ zvC;pWNQWB=)$U$8_?+0@x)A90Hc}s^gi)CwM1FmISP+cMepDDvVSiK<&0Kv{94~Zo zRFW*ueq5TaYkyqIRef6!=xi9F6R;9|(eH1ov?*Z2nJo`)t# z&|m~7y!oU*cZF?iq~!~+=-p}%WS_R5#1?ZvqX;-9katA9T6A)6bl;mL2r@`O2HS!dX>{ z2;(BZ7Lao?p|DJ45Yn9sv(=JT0U-ex76~**GjGwbdip=n73uiTh{11?i*6g-L-7Jb zfGp7%7zmPZ$XX#Cmk8!-1TiKp1{6p0lQhYvi(sSoC^wQq`6Va=CS_A$+{y?JFM6NP zUVoc{DB5rWN{j&4!Y((7hd8#re1zO}=bA98BEh8TmE7QurtO5%z#F zvVz;cVP@3ORA@mn;D9*T)Dn~N%>s65*4P2VV7egpsXVlsMPu|vR!x(mLa_E-5 zmeObmx$O|v4i}MQ!`X=Gc_{Ht$S-awG}(+u-g1qxhJF4vs8SkFN!@vYjAvidd>>Jf z((PILRrz@2Yq?-eyT)HVs_sg8{;}Bv|E3a#f0Z9(#lELT{w-xzWmD3t{+`+Jw~RZ1 zO~ruk_ z*rk~P=W}Gb9J)>%Ti-!ih$GC&I`zK^>DDJPoi;%yKbuM=Cad^`|-_PxAm#o=&IFeFps8a z%^>G?^%!5-omy;aCa-bKfqz>6e2r{os`Uyp`t%c$)qptJW6eWo)Vb9|MJb< znC6CiQm2X~eOk`AIhqfh-TTRkXH|dFnfp&pn#7LSq3?i7vYxX+kXxS(9hh;txLr zGX<*dKtJt`9#)p8yvqb9LQNeWR<|InZNuvprfCmrM{2DdF?1)Ebr0(oiLG6t-Da9| z{u{Sbtvxq%haWogHZh3WP3BOnF4-bsb6* z-S@Ohx)nYDcXWXk{hl@D>B`Z4te)iGCrGWvJYn&~}WKXp}mn0sPE)VXZ@?{9tH zzq9N`(UqmCYlVl-Bd4U!HDJwk`|7`Qf9`;!hxnVG;V-L#H=SE}L>4uj`4`c{0qbPb zx9xLXm&vUGeT;8@nTFq9q}Mes^7o$GY?;%yp>Q@u|FcKlh z=_DoojwZ|kyI((wNB)I{Rpr}jy;{i=?3$ZxSp!O1Dh096A+UXmw{&|vVcg~nkPTB& zx?R?&q#*QVg1xltt>(r*z5};g({C2EM=zqdP*S_5u3NN2(B8{B$NRg^JIWnYq-g&o z*fC#*68Xfg&4Qp%;A12U#*sOehyw`U;^rDS6bxRQ`CND7KgI)Dh61;d!ICYIDW1;) zw6_+_A0W%bL~p!9uPJRJZ$hEhz#|5LOfaM`7`!Tz!Kj&iVKE? zLg4Y!LwpF2*9Q~HKvrjhx==x=3-C4+cliR0qLw0($W?h6$P|B zGM`35gfqj2P(khAF#T?^J7$7bcp!e!3Cmj$c3a2-^79zJRq%4Mq<2V!Z#0$+gtstM zNOL->(;@G&tYp%T1`irMFhU*QS0c6BMlx4^3ezo-JP&7cz*M=-aEA&j<=8cK`) z3lP?=**FrUQl2y&4PGaP_>eHP6K5cW$KwHzzVIJhvEWd#f&!KNe8GSJ}n z*^o61&^i*V%n)+f9JvMs4Tyv^T7z(GK}XQsoHp?K1;@EQ__z?5N|nKT4SFsO81l;> z@{8mzeK;c{P(39?0>{` zY^BIxBpb$nr@n*hgrlu5;!x`GniL^Kzrp`lLNIm`ml*)CSK-X8p!bEiBQf~{JQg#G zVGbf(%f#3annE-Q_I6?DB}0tEZIEVF{HSlvfdZsu#(&L+aLtvlh1irA3h97CI6JZy zP~e3baFz{-mpEuqF^(@2!t0vCiwfO@79T)yzb}FJP?!tEAl||fo^l|5ZsCYQ60Z}F(BZhI6^g%{9dolbsaUhfl{so>Cj z6HsqcL4NCxEi^w(#qyaIY>GxGqYpw(l1P73x(LL;**M zs}gmCatR$7D9qt_M4~^c;&7|}DpXhcJ8@=JBgR~kma37tE=1Jurda24LwK9L)BCLQ z_7(WHH@t@iF@)z&d6#B{+$5dSb8GPD?4K|pb9M*MAU$Er_lAqg}AhBOs*HlY6F z@<%*yiBiV?R=`Lrcr~|zTL>~VQw+(jNjNbV#0_}W26I0tN3~YKs)HX^s>fE6=dvI} zDBx}|M41Qjml&i!0@SzzTN7i`Vh|l~#XQpr>}LH5(8a5X`*-mbHf*Z$Ye|PL{+=hZuq{QNRO+Mr%~# zK1LGlOaej)fw%{*e}L>l>vy2QW8wr>L$GQdWS6_bYw8?Cn>m&vIP z4xky=lD|;M9`s8K640Mh{ohKmvLSdA)qZqQvI{M}e46DsWIZ|dC^_{21)PBbzh<{U z#UVbOU~i*>@KbZ^nOa^9qDvHbA0y+K81x}GbpcuYAD)G^I(V@PG$D{SUAnX;poF#%s59QE%2E7Mz9r%mvL4)c z%wVx`@_C53mv#%%cY&O>1Csh9Cx(F|ltDNSz|EWb%fi6*wx;u?l4^K(UO-BpZ_54y z*pvplQ5mwA+`;Ssx6L&aAwznYL_g#?+ zh7g8)7@k9m)Y-t>E`Zd;Hx)-<=yTD@O%2ge0`JII{${8$7z+QU%usMB4zD?J zx6b$fDQx-1+a&I!{DLsxcXcf%^)Q!+gTH^>kCi&tvpPB*H|sWCjWk{;Zt>fS5+fo^eAAwBi^>AYrF?mo_ROk53=pi*1ne_|xYry^6uHq&S9al-c)In&xc<9q-Sl(0B# z9mrY)@2cB>Ih)*MYeq2?%3y%@7)I83ASLY0?+s=eqR`o_Kj8#ld|A74X^Dvn*sM^( z4z#Rs0y+LGKTN&E>Mml3e$iBbm**muz!;U4yC=bp>~%fsRz%QqYZcWf;1MEtNOECbNfkc}#Un&fqR)N^=+-ZBl5ERCW z@9htsYcL8KG~*fumRMp8G|{{U|Fi{4p^-E00WNe^;DR5~h3mN`su5DYT?-f%4kN`G zAJSt*L(Ul(Hj&^dC@yyu2sYZdlo!>t4(C#}MaO%P97Wm~baJ=k9uX7u8K?cG0YUTp zXXuhq$Rt74Fh|>TLDn@Mf)+yp@HX=425`57S#m4-cv{vlh-`A^P?Mp3&}dg$P{WVa zQA8DL9o*yd73zltZIArz_eDb4q1b7t^Db|J0dRThPG`^wXj|tIUD-G`A6@TdR4uMh zFW5zady_ZZ2nJ*>8Y*+rDD^kn{WqiWLd0%?+v=&0ci?sC!J7y37K}_W`z;ykRT)$` zQ+xDXOm39#{>>t=#5f#{WZ4o#98#W}chqFF@azNC9PpOG3Hr<2Lzo<2;XO6wO>70d zGnpXngQMi))vW;8=mK$caBDZrTrCf+8rJ9JhbL0Zt(!no8A+gj>^8OiAqCBT;g4)=+d~7<)g9a4Oe5QPRoVBH5SzQB6#uhu%48bgfO2w^GEbD4 z1BB#fgB5$0ym5?mAxvhdNAg#*)I*qOb}nxwL~#==*9+zYhder8(ZKWmb%vjvhV@48 zPyvHytA5dLZKhPu)?{w->Ycft1c23Uyd~<8conw#pS?E&oD40k&xRt$f~QBoN9t(^ zAgD#!+)?bdQK4FF+K=O>P1=i*=nogxu7s$F%QV7}72&L1PihXz>3qZV zBi{sByU4I<(;45%L5I`TR;Kl_B5X(H^EMaX(RaHS%4e2i8C<4sS(6#8r@lxAp%+Db z^XffIl*I$!$_@6OM=i(So4LN!&mw~c(F-&!yvs=7K{;JOi>1$$<2w&R!#nqbti}eO z0~L3V*!n8d;VGW=y5qSZ{`85!yqmDK!8qX$pC50&|5)#hdZFO~yTAJB)BM@=zt8Bp z9ON=W3KQ=&@~QXx(eW*S(h~U&?*)tpZd4qGlmWi^#xpSWY#u2~Bn9C87V#CR{+{gP zd)`4zQ2`z)a?@~LdCY=1HA!lhZ@db0{N`>7FL>QA6<)qcM}B*m^qy~&EI_+Kk)_}% z3T;zZ+MvX7WAR&wt937xpOo>8Plfl*gu4p=^m~t21P&^Vs=}Kpl`0|{Ahf%hIRE`` zHOZSUO|`fG0R$A-u$h`>X#Ytd)nqBHo)x`%9wG1@LXy#>seG6AeMtG2w&xr++f_PB zO;(lvqvl6BL2-30t7aYKVm{x1hH)ABsE)&+;8L8*LbIM}LC2LIyA3%-lS-VNkU?aq z&f>Cl5$vylU78ieJM)Uc7Sy}OeTpUfP8=T%lL5NSwXq&SVPi?N1#M#=pxn(myf$NL z!0laWt4SbRoR4Wx^V+p($l;mK7V#fJ-}g1~Ds5)GKyMLqx}=P@owu$2B-@(g-zOjmU!$ofvUv z9dV^FORERxZ#VEYJ`x+tq)@Qwn)YxZp0k#3Rg>SxDTxZOsUp)#OG$_7css3>no&P52)(k~SXo*Q)M% z;D|;(D`6H~rc3#3scinP;pO1h$6}YcWw|HB3^9M?B~|Ms&BeVB$eZ zUMC)^Edi_K`M;x#WvHm;08Me(`S2Jcdbdf#FDG64# znH1Lbh#I4<7yUyp>f-H|f9ZH2^vmFtS7grDf3PNgq7_uAkib*fr-GuyZZ-y&r1+(A zz060k-qg<*B}~^A%KTJwU&`al13|{>rnhs~>|Uu~^X)MU4Eu_m^~(%YP4uSy`-=MO zYKZ78jK}pBKZSbL-D#a-;{`zB6fFTUUmPDlolH$6{%R;%=TiHax7hVHtv;{RDd^wI zN=e#vYfZn?YeB=cL-0*|-oBF;x`b;Ti}^;e2>U>4uyZ}j@1|Sf%*yff!SC+cr!JY! z)%7NC;o6(BlfaobI1hjOx4-W%K*M&#QszAQ@P-j6(XEUtexp0F%|`H#TVfr!jEUv@ zR{;}{F}^OcCIO6XMemV2uy(-T>o@7v23VZ8ipMR z_WDivZ2T#}3p|qh<;NUPxAC`vGVoZV*MG4U{U`n-Z0A@bsh@>`M#J}=^#5`u90|a} z@r?vTr2sm|08;XRsOac#F_kBmDRLwNCYFFW3+88pF+|M4?yAt3(%J7TK^D^4 z%HAeJOD-+}Svh&_^!DsBKwEQ>9JfVOnD1N5NVs_XfCmOAeP?oXz6MxK&O*0<9T+Ch;s;B zau?0?*39yYmVj`kFXq2z{?w6HzIS3H@%ob>Kl^a|lD>2LQ5$Tj#_4+RduRWkrIl5! zmLn|E? zteb6UpBSE;=;__zJUBe^IXWUfJwHlaTwhE70{?c?(D-ou*!F;y_CsAIn09MbpZ{28 zeu}kF&`WID-qaS$#ihL#g9*;$$j8vQf&RLFC@o2*7j}Mar@60`#jV^Raf~RHO=h8k zbt%rhR?3$QbC-zt%`{O$8&M5l*JS>lt|+Tyywb4$F-geyyZ^o8!APM&QlzS3Gsllc zO)r@m!zM1BS~R;$wu6Ej9Uz5%MUai48p@q&P~d}okA6AKA8AI`KwC% z@14$oLdG!qz#kDu)0J9zpJD#ZmQ%S#S)V~h&6Yn}Y?h|J7zvz@_LA{$ zOy&t*O!aW#-E?)`UM`Ip^CW%}_az_?bves8rIh*B6?%nxApW__)H|Gx)%x99`@_w} z?s&n+-kv8b)t}SNCo7ec3g^gGBKAfK!v`q)d@vp$zzi>&@O6Fg7w>>=jW7`Z*7lM~ zO#KYRJkw7<$CeYO`^vJusLjM2&{6mmNZ)dh9gSZ2|5hrAoh69Xn+7T>h&W7o2+waG z?KF1Qd7ZwAdd;@eh}-{J1kuGiFb^EK)#jT{~HP@%eH2x&P{#C zYG|4bMfMXG>k^OR9Aa z4|}WePBV=7Xzbz@5F36-7fX(m#q@i|AdBe$+mdPa+v{^`c)@TQIsrkbV*uyuZ!xDh zwnDJGOW%fs77~M$9P)q4YyK^cKm>;YOG%4lI_aTXn2Xp^T-VE%S;lL|@957ySb{0| z1_)Vg+1_~5Q?b5e=16{rWm+OqNHt86m zH(kB4vgIk}C6&;<6h-r4nF8&#Z3bW@O+z%{646tUh(6w)V-)>H#y8dcOP=?uxb#V; zv#+mm=cp1AL@9EA613?qkY>)%cIv>OD#Ou$lDCJ+fn1wWM zU_USYf;yfSJo;nm0T}6n_+Nw-q`U|GisT^m<)lw>g)-7+%bdkr5Lck+U+yVL-PH_5 zigXn@I93kO{=SG30~Hhb_zpl9lB55~zH-lGea|Gs8>4Rdin!$VJ)4+YtZwNm3i!w% zm;Yrfbl#k*@xu^z;%vORiUoCVrMy7xWxV4j^Ji`!_$XK0NQIB)hTgyIf6I&&Nu*-7 z$v~12=_p4dCR(a17W$#a^%ZG6NS-w@^@FVZcWN;7rkpeR^Vd`k1r&Lr+`ejk*$cOk z=vMsbvG{amPg@~g6*KMqo9yf6rwZvMgUMjRro#VdZDlr(KSuOu72UjNeM{!A9*g_8 z=m@|n$LRkt@!X^E@R3>mrQvKM>3qrMe7TT@RV5BjA4rrZjGBUBF1ug5{7v(*>Ldz~ zJLXyOCh%z|pfq1hb5$WDS*@8hrkyV&Sf!d!tzDsnUZ^(os#Nc;)@ZC=sO8Imn}Ff^ z1!9X0xmO6I<`d;Wn)y8LU_gL0{L`V*Qpd_wt?wk<%UO`F=lw(kUfd878YMG|DigPN>-hS)KV`#f^`Ab&${WAm`m`SWPbOe>^(!Q^`y!zZ_HlgeZsjQx;C1Ht zXM7WVFWmX-UtKVs$`-P?6@{;c42@ZtLtjw%^bsdsz_d8F35{-hTyO>=geSKD(zgvT zKnJDcaNgFK16j*6VSG^^Q7FM8?|eH?#K{<$5NRzn2n(C)o@ z{-f^g+lJIEiKTnKyHRza3+M~leRd;icmPWFQ{BWq_jAEv6G&r58s`C@lmXI6s4+JO zbRgK7GtBn6F+Yq`A3S|GCFackH3S|-aO;Pqz0qivlWcWKc;8A!EJH(u!jPoxXz>61 zS8tO6v*@}I4{C))z%Cgj3b?NX9))0L+hdSkysQR6WoW(!M-+z^;gPoet*ltVZc{Qu z*!DE*96p75pdokwY%lR_p0ir?d7qrh&k!JJ#=g8<4D8KZ-9R3OUH$uXzT=!dr?Gop z^2mU@c+hC>>%$6Gz-dkxeA_>fWbWpT!33Ph zn-NMITZ6j_26uLBzKkto4Tm z1RIG5h@rKAYW{<#)brOdp`u~~Xs~>wMS&BjH>7_ttTJ%Le;Cv{^DjSEbPa`I*5YC^ ztB5bgjJBx|aBGLYK$BYq+&0_!qfYCwpJRnb&93yJ-Vh`QALyCdmoomRWfhfQ@!F@$ z?mGFm=Vsq3OIFp_36w)AQO}appiH-0^||=G@efH`!HEjBdHIUZ3-bGVpYCw(V*dm$ zN!T|3;y~L|u9+>S<9Yd}_0Iq3Tov$%?jihu5t&}{2!Z0asAvoU-XbDR6G>WEn}0b4 zC8UvZ-Rh7`1~GF_~L`2>dX{LOvhc7q>D17w*1tl6P#*|>5M zR3Un9T9hFaXuPoSNIvL=ClzHVZnSn<7L1_sGa!vh0tMhPcjvN^TE-2#JH(O_)*hyy zmcVcvs-RM~eM>E(ZC4ly3Jx`51tbb-U$ca<@OiOTcyROJISk`HhVmV>hVeJUoJF+b z@%;D(v0uC5z2G^9rL?@XwPE&)+7tP%h?^NDOCXtY z$?OF5+6U8?#e}U}Ls^Q%y=B8y{lp_*12yYd7o?qh1WXM-iSNo`=g@iN#v`Bg3B(?( zw_vXyY*&?)$mf+%{Q=7#iC15aCozL95gZ5TjxIds{}%r1?ODO#N+nuI{l1BwAVjywF|mwf9BV~aCaLZO&Hde@q?dypdKgV!wNm3QDPv+pas93x?u#`(i8{J<_CIPyz$ zngpKLZ>lJOU2Fz(7Q>yrL~E2khJzog7LplrU*1{9z@ua+Eqf6gdJ&z>YHv#w%O~h1 zG3pTj#UP_{gWly&9Q$VJron!wogMhX)H8}~?Qv(+V8KbCrCjIRtO=iFi7OX7?+g_j zTo`o$OwqR?Go?#0<2QAI#MT#Bdja)v`k$NvOtAtGn?3btGTuqSr9qPOGBkoUS!W^6 zf~km@&>mB_h^jgY;A1gq;@=8ob(99% zpJ9ogPOwmJiFd?Tv6T|-vf{TirFu*WGKQrl&5824r6FELN-L$R`^9p{rJ7@55TP<% zLv3mQGPSW1!<7<)m9lc25)Jv%mn{IrWPrSDxm|3z{YtsxeYpcog^gH+r(uPoJwRDq zM;=-3y;AX6sRBk*=`mIjCKj*Jj1$ici0P;_8mWw}u1p}TN;;`bU#T?8#$~m~fD}Sb zwkS`M-3w`|3&g4qq^pY!t1ENeAs2ZJy2YfK<;ukH1~GV(A-shhu8#!hC&N3d;ay5_ z$K(pli1M~-_|QGPI~LL7kEqXuPu?S@auNDa06w%*Il~qWO2#!p+OJmEtligatkkTs z*KQcr?h@ASjoB992K-|ye5@+`MN@|st^3JdCmAgE>bi_L5=RkQ6BL1i&!A&H1`QHK zRu0+!$wrb@ASV}*1gA*g^Lk?O_|!HW4LQdWp?cB>BnXat{!mYR>IAYkYoIEo90K6R z={vgCvpMKL`q#@q>!UMjH1?f{PV|N;&F?#)Z}RjVx9a4doEHm?s{0W!a*e81jWL5* zo^56j!*az!eaz=N%1}UzT+~KXVrFHeFBR9Km)BxCh+*h}*8jBXMC{raIM~E+-NeA$6d~BGs?!_@ zY;@^tc3XAwh|{;MYEk+F%*&-VV0p<(;M(aQt8lN1krm zYMtVTHr>@EJF)IX4g_Cwx5iZ`NZyl2JVsdysYcNu%ojR zFdv5zVA!mv-o2%))7Ggy)2Tgg*u8Pu{m&kEUQA~%PRE&}lYJyKRX-*}-?cKbFVmsd zf3S=0x&!MPr65;>PmJQTDtBb)(L(FgXv6yl?z_< z)gw>dYrcRz%~<~Dq0eXB?9~KR8HHO0Y?qQ7kmpHIhV=_iXv<@u@IDM|jJL??45s>a zDC_inV%0K&#@WJ#+K+m;wgv@#+AYc)okVo#C(JBX&2SzE@zpyFCi*qbhAnYM0I)%& zg>Os(7YXZns2{bdVK=haT?2EZqBedm2O@~XMP3bnoK*o zL8}Xb&bW{FsZo6ONhJ27kVWmV zX-pVHIn8Sbc6t5OQLPF2IH}$8J3p@+uLY~%%u}b5Ze6RTZC-Vn{cK`8Zh2awLt}p( z=%_8w)w;Z<{pz2NVRjF;>I`f3_$YYgoY^42H5rf1AY9%SPtjJJX_@?>m^2YOjF?b6 ztD8fg%zd9&w=vd^;hc_K=@$92!^5$r#1vzE8*7}sTNC|VIb%buZQ~aeqx|bV`DdIn z#z6`YGXcXnJ-L406}Xy-POH(XkZMh*;Z^{wd=5UC0bV0`I|Ni4l0;PE)N~Xk1b3Nz zRoLGq&0HW5+G$kjmNf3}SWjYqtK)mGWzBww|G41I-A>vSYTXqV&b^d7IX_ghK?-rx zSUyTbA0^Q5T&*7ru1(z;Yd2P}%j+y=e?Be}e`;h|6sXnS6gxTka#TdOLw`DyJ9R)( z9*_8WY(Kf)kf@va6Xi8%>kHls6g*bqUN!_}@Pcg1^lB~Sd5Ylae#Duw6gc%GH$p-7-xnY@SpmTy=42^n4n z>&fXC(3wlmbaGzB+(bQS&Ay|0tlj^7|Lq8X^EZ7Re1`*0HvFsl3_eOTZ{j?jB;m^0 zX8)OD^v8(}zy;SoIN;rj(Ikn#5^p_$znss%#(ZkEGCsV}9*r)JOxD16NF{fO1pXq# zJ^em)*>=C+O9MAucjSEWgRys2c42wGYlq_hkoA^PQNCaN?)1=-lEMf`Np}fDr!Wc# zsK^LNgF}PV4BZUfFi3}hbSRBJ)n0w|xqGbFrhfb5#pyeW$-$)QW{$s}FKhV| zpaSEEQy0HWUjF5poY(OAle`KNyMy_a)v28PN4OF$g2J>%SH<4&F1+6!<&t*p8&EG> z7-W&M%9rJEZC)bykvlQKBVy9EfweRyt={{avFfG?N3o&9LA)L{gDQ;^k3-btZjxY3VwNu!_V_AEn( z1HfL~xp#12fq`P)7VS)$A7FOwWepfkTiL&)&fD?~XC_bHAl;THO=VZ5f)6P^aUWZ*L z-CLP;HsF(PdmdCJn^l}E7Fk*xcmtGO0+_w9%68pd?}O11)15Zm>OBmC_!O@8H@cr5 z-lQ12h=;_EeOMy}Bn>KLK~f?NtnacfH}BbOq_&4pE6IWun$kWcx#hLGuhu&-40z`G z9jrFFun$|kllR!fhvqPFN2nd|&edpSsT6ej?yol4wiVhh7AgrJ;N`4U{so-uF17Jb zzYe}S+24q^O6JE8IH-gFeJAn^Guha!l*$*QFS4G6klSZ#N9oz;*cZl@8rD;wayyt>QS*F~^c0WP6UKsYYxSl7SE6 zoXT_eI_UHALMWUo@4pdrt|~7SGC|uHk2%-Wl}8Mh)lccWyy?d~yVP|q=T30c98kD6 zR2&GpzWv~y>nNA!+7DqE&;8}vJY#g%t>x1*7q`|$_Zqjh)sTI+c4E?9_m0iR0oTqQ zqd>Q&pRxnBUBCK28ofVSHF(*5dJuT1ND3=|*?W2#FV<`Gk6pZJ+qr9+kGg(ecyLeK z@Ymo?g9MKu)*2;R7XKr^2~Mv;9Z3jpQG(}~Q2m7GxM5pnkU!*@78^8CSGt)>snbmc!nru;XO zRA~Y>Q;m%Rwlb}g0>0<|eFJ*UWj!$b{dKa@tKIA2$*aBU`l(m@xIZ4tk_$(~S3g_V zlL8Mq52peTvFq^rjWaU`fxkX*B?tW;6RTG^nyOS(`ZH&29DFiw9oqS4%7X3Q>00QG zkh5PV=)k2Veyak-C9MZ8d@uf{|Q4?a6D z2lhf+F;UXB&kqg#??SZ)!eoCG-v|{@m?7 zQ_WoJ^-N7yUgNp(TjD(s9GCHQy5;H-z=4py5TecQ#hf66^uLSuO2K!2$pPi2@4%NTGoe zU~`-@V;u~E#flUKw4<~^NdqOk)JTFp*(U&;W(UOi6os{`{tbOZP=ngfw6R&-zQNVo zOMxWHam)~*>%4uGLPtP&dv76A%pKToB={!rYoQGGJ7<{_OnIy!<=XfLe6$O=DUXCA zdIx9#M>xh(EL04Do_NVDaDLp>)Pa-?A{z3(D(*I};~ZYY6~NR2ii(g@JM$_1Wj?$-$@ zXJ7xp8OA+MZ;Si%HSL28R2y;DtD_ySNfxQ%b-#FT1W{SNmQ9kqS**#k2S8As4{OqR zrbN4L^6~_X>bN(i7EsxN>VFLo_C3gMV{sryOW2bGPhk7i3$gHk2^&$b>}f-L={$u= zSJ7UI<5(+}sqA#5NFNZ5h*I7Um<~EJQ!cVEpWxHp$@QSa(< zUF3UC9>fajZaZWmsh;gLSEy3&q2eEZvccj=x+tJt&8yF~?k%s?uv78|1GX!-V3EuL zQ4jWCExbDoV}Vht26nz$927k*HW5c#>I}pVx1S)qzU=a81};Ct`?gfM{dBd>TP}pP zUPe%3MK}AXSp~rClC;s1>91|Rxv37~ zC_O%CEB8U)h-wmiaxfnvRo^k9bMQMo{vBZ0=rn!OkIH#&F%3i3(%KUIvy>-zm(Y63 zx(0WzK23xQiRVq{pEsG=-`cPFaWVU@&PQ4K`43x`0S5Z4lWr$IjFh}|?Ol=W2;om@ z491;9ha{(jz{gam_4ZFz;&2fHk`l+?$I+>xKz0ToOK^C11F~&cU4wAsMD=an77t^n z1WW~Ms7;qCYZoUqE|FRyS}RiOj@>FkPF^jvwkr+m%$k-`zz)W#M8(ckGk$uj9C8rVQ~K1j(>efMbTRKB997&uwZr6cMK1dF!t$1Z97rgM-E#$8p z4H5Y3dC@h5~9%}K{3J?0hkbr|EZZIj_0!sZyR=O1pM8~!;n z{M(-pQ7q?>9FFv!=Q=_y!r#kDc!Zp8L04QpT)#3xk|msQ8u7Ixc#1JB)z30|10aQk zdk92(2}DaXM|+8b^Z6C5eM0F4LZ-!EF`wz_EJm=AVWUQHaDT^#f2HK`QUh}Gk+5h! zomgpRuqFyD`PX>YbD=df)7B_DdSV8`Q*iTubC#^iyOw;gY|TyK4~Ydr$vbpJr%&dq2b^rEn^uZ zA!L`Hr$rrmqYRoMFs^1^u6b}}Fh7ASxM{4ISD9gxk4 zfRCC2#&(`@io>ltc6t`P2ox_MErG!^StBPTJ91nbgvhi}T*~)dz~6{jP&&Yv z@{9m^Etn*8R;bBZWEBlAkqB^$0jtgclAYmZ7f=&anGXTziHAS0hMCcVZlghb#3}AVj&H7fk)6nyV#N5F3PA0013bwJ^<)80Y-s^-A01VP*4NHo7)7y(<3PHG2|&8tO}^GZYwbW z2NY}<7yD5k4@71WBNIO5RKuNhs~tfzq|DCw(oYPLP0SChBf;a&W$Z^_2`t2n4J<|g zJja8b(V*qWP{MMRR&Tj3;rgi_-V4VvaAlgyH$BE5#K}21;r#9pepY#d(O?L%5ki1G z5U(7F3qR5`OM!E#%9l~oRzX()&PQPLYM4D6Oqv#y&jvCeR%z3M6bJwl6uct0x@HfQ z>JSjLA(sjV{%I=-_KGAbBziE{XEx+`n$|zp4VJ2D9o)lx=+ysC7Y|aZhCC^gZUD;= zflxF^8r`h3Uv;||R~+4q(3ko_@zqUc zN`djqEjpj;Y$9wGS|eTb>hqPNfbE2-rTULvDN+~Gq7A^?D9FY|)uIqwYrIXG0D#Z} zgYw`qM-VYINP+;&r>K@bs(Oxm+;iEUq!fx^m*c&B7BEw~aMu7U#6;X$6Xz#E3} zO-2yO6WdHj3&gR59-*OT_~a-l+`Zf&huxws-q#EAgsI&g%~#>mckewQ114Dn>3kk! z!h-;`!1xX@TQ88CVA^>HE`|m|5pWX@pj9snMFld$gB~4$RLokc6pBK4(qjG=Wa>3u zRP<~ql!{inNYyq1uR0Qfz~dm4&DLlT+ez=@GC%fXk#b#z@rpp6?1jfY@L&}I+U!cDhi3rpJ{J~6fn6ERtgR6}D zMiSpj@!#12V!wd9!5zS+n&_s6(WVyXCOE>GBv-d-@a$VU`=Xk2;-i~tPtSp7?px+b zTb6ZOPo}o44z{eHZE0eGcd0Zd%vT1)>mN?3s@}Bo)s22hv+W_e?Pav>Bf1^M9IgH# z@1ZHqR*5{j=KD7W&rSs|A@kViq@CEho%pGpSR<&0I7Cv%%cWwS+HXRfV+UlnV~X^; zJ-o|%v|B{8mrJ)->b_T!v{zQRS5de3W@@kA2;_DIxPgQgl02}tl+zM+JzB7a9iOO1 z0^X~X8N5+#WX(V@d-7KNxKrUZ+5KZ`>c_O`&sn3NGwwe}lfb6fj63{zW9m||tD!Qp zctI?90s>;eKWs96(7c#*`y}adNOcNAa{WV+iz(QRH+i>r^6Obz;;dyWMk7H)0$=Fw2X;2G=@lMke*%y|^TW?f*(-UXEP_ln~EDdHjYG|-;Q zlsYSuC_3kE{8u&o@B`J>KdZli)9R9ser1jS#aV0nv3;iGJBzMY4^GmGnLfL{`B!D_ zEX5khl9?H)`aCuH{6^`SDyitapkCFlK1}HO`SkXA*|aK|&$};f7d3|}BKfCY(-&{8 zb+4mz8ed+1(l`%uTWOoV6#Zl)w0_z5QaNP$Fs0$NKl!Q?d@%sH*IRTo9javi>~hBV zUmee@0OOO-_5W)AoK5xoTQ^p$kh$oU7{$3M?6 zEG{jttbU1@oJ(p;>H3;h`e|)7gTs2__hjDRv0Z0>?a$9+ca8wy#@x4IT6f`h z#Z!*3RZ(zdFNl7A_Smx$&r8Wu+y0HOdBQIKwX7X5>fBU5lj^4(FlZDL#I`?Qu!(On zPuA<(YE={ebdgFp=$26Pbt;#38maad_jGiYe(+8Iw^BD)E2Yfe&7hfFOiR%{anuIB zwM&hT)AO26o?v9YoHzj6n| zN}t(7RPVBumX?n0uRK!?XKDIyym1*h z+2xRHP}<2-YIWy4A)1?;IcR8t!Xk&>4P23~ z9_!rwTm9tS`*#76kWZ5zOY`%uHq6(5LzFZaA#L}RotsT_T(7b5D@S;>u_GoBGGSwy z+CHrrbG+5=^QIHhn(Yd)q(TM3}GA_>{A3yLCAGRiB;vkEiwO3DlCiyIpw zq>FQEs>+H>+PibhYPt*335_56Mye*e2%Q}nA10IA=4Pk63L{(JcMUgG=6|anZeIVs zNPI7GaCr3V_n$u#d7b^li?82bPj?Lb27cnBPHVBdL>FJ03tQAYYL}ml0cu8&F&s;F z4^;m8!sAjZ9F=hVD_tv9;69sTd+O?Fs*viQ&{8#*L27~Up6O7k{Y19ni~g(KKN|)a zBJtl7I*uz9it-cKyk1EOKG26(s0TyS%I8bw8|r?Cwcn8UGEE;<4x4 zbWXCY1YKjyO=RV;lDBTk-vi>-{o15eP4C^Ek2TiQ-S7WD7+mGzYtxrR=0hXRA-@Pl zBmjGWT~PN~8$$MSLRZDnI0-u#!Ra7B>TyK%bAjg`y;>u)G%O#`oQ`yHj_a`k<(1#! zgj;uBd$TB1e5GNK0D+)#53x9%#Ua!ePK2~Nfm^y63Gh2?CIP}ga>VQrP5NAi2TtF8 z8i&c18pLfJAOb{NDxL;>4jGG`hTe_3g1}MUaDFH$AX$Q#=2}Cedjy3z<&QiY9OD|QA`%k(MxsRfg<{!~mcy`?vDLR^}^%l+s*pM4cpU00~W zHQ`OGw!L(+-!Lp1*zGr0o@O5_@40aR_18yM8n2<$1UTe8AO0G0Q6vnp=&y#^^G6un zawrJCS}x=4d1O<}^3dU_1L<~%EO!k1D__BkqYv_z7eCq#NkIczunL|EOt&a7mSb6# zchKNBU2g%Qh`;-|r=D*3RTKOA<;{^^m?G*P#|Ddd(5G8G)b&j9vO&goSTlL zw1}QAN_m56+CO1&%=GN`^#UE3>n~A1D;{pTfDtMB)Rb32rQAK) z!*ZFRfYrB)`9Y+kDpP6^GXh7hfJRQWu~kKyNPCiy$*BH+m2>HD>|SU!G>ut)@XV+f zyd&}=XZ*>*!>o$@7b5ix2G2-y+5M|q!dVIvwx01%6m?%nKSA&-!~$|xJB?(vAo_Hs zCh6<%>}3+Ar9C{yv)|i&mq>Fm`0q?IXVTC?>T`#oH)m7UZ&x*;Eb?ieZ+|iy48N;v z>lvEweJtoSbx`>;I^}y6c^T@ZHD8JU;XpgiqT7s^@wNYG^1M zvP$2i8kpIcUMApE#g1LnQ2#ko&OIo2-_{?@SxN*c(F1S5G;x%x#rF}5y|=J&DI8C+ zaQ0rLF?ksYOZk*=pTc4fCYt)0auWe#Az%XOW{?tI; zoO9l%qE+#$)nt=7%X~}ycMlB%+vU0b?oVYAW`Ynb063s{`W7!!%g!mF1f>{O$nv2s zE7w?>dUdK5mRRe^LkR>)`!-i*{BZccFnFd*@qIUEgTTj2uYDWqC*QdHjju2a&9|nQ zf!s0xtFzx)+eYr!d1kHTuHXFowmG=YF?oFTe=vB5Pn|a<`m+q0i>6!DP(N5(?@9}FI&WBBcc4xt5TfQ!3^!Ebh*UBlr2GsM|4 znV_GsyNgCW*;~JW$qye&0N~xoYZ&}|MAt|%RRLo&4Z^{FVj9yc46kAE^D&F5#>_U3 zE$Kz_k=iPZhuFIfuJC|+8jl)t7v5dt(XAjqVq^Zc;kKg5(zwH;#BAJ(?Mb8D^p98o zdxA2Ao4IqwTE4lM;k~2g-Q3wEK1sMDB}7_R0V=9$E{(_j;F3qj_H{P?B`ov&P_PVlGra7tucSVMI2i+ zpNJ5CvL$j)z)#0;!tz3)Y|EQOH%EuS<YrJpLR6EQStIoooxC( z@dfpb?1jcVQVo%BIKha^byw|U=G;}J{lZtfnNio`+!D;D{8P?T*-2{5EsuZS0$aE!A1uNezEDqLJMUzKe_y;K`R#ti(-hpulvWut79EKl)ikRi^y= z$dK?(=DKZ6x3)Gt_qQ-sZYgdk>fJ_OhrIL2zjd_6dl|87oBxBW%;E6i2!Z=S${eZ8CV zease4Squd`l2j<(u$|F(PaS(hVa&Y#B8`hQ zB8DOKSr?Yc@3?KB&Wbx9KYQ7Ch&(Qp`B`6caJfb(Jc)Vvej)3>$VKL$HM?ypT4#b> z)WW`0;o*BC?uO;uWye{E0%@PTAjnp!2#4{#a&SaMsp+mgqujo?UOW}NWB+cV{P22- zY$6DsY<=>~FND$*IP(|tSwiL9fon-F^p<&O(pK==3m+ms+vRrX7C+{{Xh+bQ3Iv7C zvVq2-EsyC#joX6R;6Rb<|8v`xfrb5>gKK>>E{4>GJNW1iH}|F#_nZB7=W;gua3*{o z7kp>YkKaGEHxt|c2=}u)-P5JTu#MuAmx0WPdI)hg&|!6Fe)+ zGcn8hJ38X$7B&isTe*W1X=l|xi;$Fuc=!` z2w-+Rh!F*3nZ-?jZk=t&e$MhTmWntwkJudwd%4IacJ|WXDAs@$0Gf$#DUbd!;JKH@ z8aeaGoQw^D1q%Yq{4j9kUT||e5Y++>!|x| zdc;;+H;B?QabYBaRu5v01w3elMEj*{+ov=RM1L26aTF)hw&`UPtVRn?*-XW zT3Ng2Lo)8*rCWI(y!E4*`Llu@I5yb;d}c>l*>K+b-3Yy#`6f|d?a`e4fZT?UxTL>q znhaqZ8zPhEoWfY!%mwwlp@e0fh|C=PWmG}&EFQ?5jyeK60i0Q}S;hi|_2DU#z#E6r z48LY&#~0%?9q<{k&s_zq+;tNRDhqlAv%Ir`4^F`Vb4SRsjG^VdpB3bY{+l<8jfc|o zIX;4fNaE6auR%hD*G6_wn@09~<^dV}MO6NI+3jI2u|P%XY9pjXx|dh01K$e1P9w3Ibe|5X0U83Y-Cu;{haJeEHHc z;BhtFVh<>U%Nj17K_bI#Pgr0bu9ca%yaubv4}cns{%KVE%n4ZIOdkzleU! z0vVUnQO%00EMHt1iT^5PV`%*fDk?hdYn=5O?{tBg_wYJXO2;6f#xBc750?6o`O+(R>y9}#_j&571h>#k6FV)` zcLsoZVTO9q#y$gNMz%3tPkHe1(E&heabSldoB~m|I-6qRPsSBqVy~Bj=&EarYw2h2 zuyX0i>+lham)Q&mUrm$anFGzo#dG07erO1y7wp&zWb1fH{O|x671#=M zeP#hIepd(GQ8CtIpIhn*IPFR>C{D_=#gkSV(=J;AEHM|kDW~Z*sIq<@~8?za7{glG~E=w#AwXn|4+DYPTonYjj#A$O&1m8U%k43B(|i{ltOP z31DR+^pdO@IX=vV1sf=X)s8AXP~#$)k(dPRVb_So>d19j&JL>|*={*|VrP>emKcXA z=mVKyKfF!@tSpX(&<>IWkq}4Z81)R`77DJ30Y93-71Y`oT#d=2;pF1u3PMA<{6kIp z1@wwNZe*iMcpuS;2ywCx8ojXizbZpp%`s2} z5^`$>sELeMo`I@v-7#^zi+RnFn^O1diS-YPr=gv(Tz>ERoZpFDL51K0Hk8~KUqgr+ zo$d1FojY)Y6H{7oAR7TvkjKx6glH3b4|hyh?ujveRTZkT%<&jf$sOtuo@QdHAEf#) z6fnxFH#DaM(P!$8iEU#Phu)n5=o0}ZM6me`z@iEerx$PVnGey(e`HzqiJ^uPK9}&W z?sPQmw=&7`*=3&aYH3WltA&*}S--bRSgN$RayaeHPhX9qX@N{g@?!d%0#4jeUq&G( za-kf#^qo(ExtTod)4$(=!yb)RzOgsdw#>ZSzuLt58HW^U?#fe(;%)~s^>GlaTC!O_ zmjy2mBut;y+KK!_LKa;Gn}o-1xs6!?cu?^zm8S6@S3g;OnYa4FF8*cqrkmiv>g3Na zWAA~yLH2yYiDmCuVW`ieS)Q;X%YQBoN4M60PmUY^n@U=(+oopYoQJug0astv|NFUq zB|-$K5P>R0Si^KW6e7aEPD3I-Jq6xlTaMpfIj6RR06+})W|sJZ@eMjgbKe2lfH5%!HPGgeh04e27hI5y%X~g8t{5UF$ApKy`e(0d5^SlohqvK zY*W2(Q@49lb8S=SK-Bl)(8-$xY4<00+%^`rxE$d*juom;*0!F8_&z__vZ48IPug;P z_T9$EMF6I8!BE)*ZTR`w!DTpK(?5>6<-wAmJ#F4h6jCR7G?Zk-g zB!uk5aqb{#LA*%NV}yQ0$he8X}vI+t~PIxnIY=|Dwoyh6=jbW185~vybz3e(4CDJbJldFuiNubAmTK z37R}X-#C2#@7HtdQ?jB=ikGL(#)*&O|D3%)#jJ-t3oY=l{<|kMCzwq-_3in~=)V4H z`Y-l|y%$$0&iYIt^Thh)SzM9zuc_ns|IVD`Pt9(er*oY%9h_wpoliubs2Uw6mV#k9mV!%Qgfs588EaA4iu7$vkePc=c z$l}f!8LOazo~2VjY))0jsNm?w$i6g35Tw~r9r0Rsz$VUl*`gXtVrbIP=xwx}yVJ=7 zV&t(=S8Kq1r%Ye$m{|QE4F2VZy@i_<84%(zY4wwjnp0H8NVIUHf?$yn7Vf1smISs* z{hb0%PyGh(&oK|YvLy#ek>5CtRc!QE(yk`IY2RX&#jWFVXy zLN#O~MzU-yBTe>GU&BU!Bn4AgHexOk7Nsb6n$wQ|_8~@5g)z1=E&KyaReJ8IU9FfV zk&;~qVYa6O>7>@CAPNxT$`;?+jRpvhim>Z7FVJQLQ_6|*)B2E|;i^U=iV&SwPam}N z6d7{MF`oCT+3HN+^tsM1_fi|YB?h6rJ9VMf2YbMHMcTwuDmL$EIV?S5AXkfZa^ctM z3YkoWlRY@^Epr z%SHHoE`IrF?OmR#7`28Hr;-K1sjUx^_}2*yJMrn89@3&X-d_0{uHyHz0jT=#Jmm(p z+5>N*!_GajuYAOmiaviv`fraS*xUuk?n42V@=Rtbou_(ZKq;{yk(Qz5ZL4PuVh z*)N2j?AR^1VBnkI`^3GK(#7ba#4g%$o=IO|o{l*HhGe9TAYerap8oQ{bWN+-GSeJ^dtvd9gO${bXmpT-p0} zy!}apRJQ|R+W01pGa?NWqW2aVnv%kB6Q*Y8ZSB8>LO}at`)KY1!bCrbg^+80 z`M7vTX3@XK+4~3ELbLs_a>zJ|wpu~WOxw9&a8x{E)gBf1m73}oT3+8w)f`zZF_ulDvU4AsVFno=U2 zbD5@Z%FHSrk6$cJcx0F01RbxLF~>s~Q@a!Nb=;%mLjD}4$M%)EmO3dPXu>}Zt}Y9~ z`y)uFwPlL?+}4(zQaz`iOB)BPpO?ig_Vcv{o0$&k2Y-+rUve*&(fH0@;wyN7i4XG` zwB@|lr6c_BuC4RXY=+T({U>|ZmI{=cizR!*!kBDpbL3(l$1ib*^iwrgk(+ObilS5M z3@>0mS`uL=+bzH^^^5*G~ z*;DSHhB%5By9lt$5X5Mn01g%oK8#;4fA9DHDQ|ud^ z|AUGK$EA9^&1u06j3VNj8oagy-gqah80dtVrmTZe>V0@&F~FAE5XCt{KU_A5l(WV3 zvtx-cZsg`Y-K)0mOOtTHLe{nKIZ39A)MVnkfG-#dRrYaX&&WiHQ1W;O)`hdFuq)!U zJ0AAEj7U?2lhB4bEhXAKGRvi|;%v(#&dg`nKk}DMUqD4ExHM_@ui$uf@MKbT4Ipkr zxri(lXc|rcA+8e(B~WcFD5MhKr{Ws|C@dX^-8^H%@1T8GM@6vzqE6aNQi7G19dq8| zA8K)PDAkW{l_5AYVI<_gt`9JJi+3JDlMvf19EK9zxAD*u@9PjrZp=dZ==7y}OP7qm zpj@x1fS{54*cUcE!BYfyy{*KOn?y5y1kp3pq`A(m-QU7aPj}b+cYI~xz|a1Tal1Z} zu5H)emPS7ES2Rn)xvLvIfcFF2C%<7EI}d6#KuG0)Y6#-#Vh+3RJ(tF`k_6k)iwzdKWu2l?B zCW6K2m?Zkaeholr@vzBRsJt3P83_n)1)@B_i~Nk&4L^eia8Zs*tC+k5^-$4CW=uaTQ~PD__|Q{qotF^z?VIu5oju2M~DQL5rPCZ zrGP(RB3V-+j9AF|+hPo|BOd6+4!?+fC_uiv74vf;{L70-B>l7a50SlPl7WArtk*3z zQHD|TX;v@D>AKNC0(dZRD1wfC26EkZBs7EfX*C70KxP7~$uE>HAb}b0jBS4b#e`RV zhpP30eDE|m1PAeQvp;?)jc_-BKw#ga#HpKpBlA83vPs`6JvJQ8%QxJY9Tis`1BWZH zPy9tvD(T5i6@?do71?nUS$0`y5=aiBQP}z-jt)G)4+8FjPg=p(ykYEuXF=c01L812fd1PqpPqXMN?q^$gzJ2eJJCQu;DYC zX|2!^c;?`HU}A%8LIq?vuJFYz%tbys6G1O}`p^OzKPC&~Fj+|BycrdNUV!;1z?Rl} zza5;i>BN_@1)$$Cy+YSV`>{aeb?HgDpmAdW?8* z`y+r#1CSrj_?wgo9zwBL$%0rgE~-#PmWCG!npA{X7ak-yAp)p~RJ`Oj$Y@eZPfH5c z%MY#!)cYfqEW^Zn5{T2vHsMaD)l3%r<@VP0S))m?T$omxUQQ;twX}(Gm!rDXl7^!l zC?%an{Vi~vQ8^AnTc)Ye32*^M0>w4uSkQ&Cyc#hq***4IpJTE{^`u{&qu*^5$tW;{ zko(4D3K-jFk8QiXh_QQ?Rv_CiC5_C{YJl451XwL8Gb1#G#X-l%$(!ULX`&JW|LT_l ztr`+c9b3esX-oltU^Ku?bP$On)?LU=76fQ(DN$N5Q0%nGPp`C1NBWWpEA<*7q7WE- zl25XxFVOH!ueDICiFuYX>S~9FYayXe(AGg(Dcu+o?~fGFa|%6E4i%!(vJ{VADY$^q zqz7TIC+!_iHH-=J?b3&FYk+sw% zwG*A-Che3J}6WQ)HL>YGzyM4y4Y4jbLEZV92Qt^zlYXR3Dlk_KDAVIl5T>g z$EHq^7q*pal%rH9N>qQ+>FtbE25uN_$Pp$!G?*DQgfjU_+p_;bd;I*~fTOSKVJc=K zYY@Cv{q;+&#%9nHJXrmTD9MfbX~8`Bn8ghzs!?4-Z?N=?v1l$h)Bqs7sPUvC5=!JnBLo8+&}@pS@zPN6{HI^`U$qXCL8}jM1C70w^OGEE7o{W8DD$otNP`2Mmh+8|)wLcZgeVbCGa_HRr4 zZ$il#TirlVmSSylTGg*6K>8?veN5iHiT_^NPYq4IRUKfcRLe)J(5 znN~=vP4dN^Ntg6D8|iT2rv>{RoANxAv>-cF7PoXrprzDZ8wC;s_3CeYAgH}Y-8IR~ zaLC+s$TDH*N$t?%gwVInLrYspaG`X$KubC$nL4T992Yf*@!K;u>r#E*OvXuJ&8jOD zhtG!1KaHhHHrCB|*5Rl|!i7g74M(C~M`C|G3rHC8nHWy^F+Bc6Q1M6omsyxA;(gwV z38m|p&0@XIXbscY;xOF7d+CDM< z{_18^!iR3*iGh6j&Z|d*2@|8rG`;V}$9_yqzoYu#if*6-!e(F#36o2;lS{4ucdE(t z1fJ`Q#VLbHL7m#~psC#-ymPLeCK|PGn|i6J2E+`fPh3eY&=qNMs@`9^|hj7$cucr(Ovv>`z19(K!bd zkk^hPgWQ!$pr*(i*Z4=-mipC5IerSCW_CjfwDX*lk9HP#2;9DI>doS6BkMMQZZ<1( z%9v!}C~?;hnJAPV)M0zKIFG&Iw=xf59I)nO8~WL)=}ftWs9^o?evM$>=l{drTYg3L zhi}`{4Cw$v*U;T4DGVJV3?-r9&>$h*;803;cehBFAl(g$h#(;?DlH(+_vUv$Ydvc{ z>v?^@yZ3*v_gbH|_r9+4JPxo--K8@7aq>`ql|AW~Kf{^*xo|lQ6v(D0I^FaueO^U+ zMiwvEW^y=qp;b(B^x*TOYzCQrK_e}srVI*UNK6hPAuQE+Q}}S?MKcgX+Xn(rfj_Xf z_VcgyJzD+nbal{WmF{z(Zv%n>N8oU^__r?dpEHHZTk_qbd7rY65~JeQ$9?1iT~4I{ z>osO+bo-GPG163$m61dV7%y1{UiVDWCMYs)H8Juw`4GMS$7S7{vC1$o8A->eXjEiL z^4XE~bH7%M#`FlaiScd+z#2P5&pp~hB<}jK^=s8;cTE+8&=3<|%Ak9b?9V2-Ym6g$8pPE-BZ#{2E-Qq^8=}qSkNAW%a zqrTcTS_RwwLECB{zU$6xKl-(;S3i1UzZ2#?N`>DhS^1)vu|eXnMrS#b-8}+*JLAO6 zF*P+$kg@B`*t3!Hy+ZX>(#{es*>Ljo9xQzcUAl+iTT9YfTRWcR`MlvV)0}kOETIfz zb%Svh%^jB$3)1ZiQFaE`A9Q#ffM80rYeg~C?`#vu?L#a~4yW%eh_eT$Q#daOTs&@e z|0D}x^9G~%GHSHGF&`Z7#6y7 zD6S9aJ6;jl2Djt}iykX=B{4uZhPdtu03UOB=pi2$btNNndw>0sWbYL5#77ZP#L44; z?a5`LIsyz2r&TM)|6|^(PGEM7?cqUhafD&+kT%3$bfv{hnt@cR^R}YLFA6H?XoEDU zL^L%G0R^eO&qz+q8Bf`TPdRl@jl0JppfxXe)Etj}9s-rc`0Z~(=n-G1D9-08a{558 znB~G(oB|!5-haUD_QdEd5d`z{D;dZNKsg9T(KZkWt|Xmrm$Z!}oA1tX{rx2*pcG*y zF2F(#0XQ)n(}kIKOFS1;a-lFXt7bFWqpopRd&OLKf%3kXC&+>GFUqKaB*3;YHdvX0D^)oKn0L_K9PejnvR_Cy4BP_uAdE_?QV+O zG(cHV2PF(ff#CTY%5--+E1Ohwqi%2f%U)XJCP($f>w=?Oxx@bg@@MgB^9facc=3vy zD)n#QTY4{E>C8JMlIFhLcNUgaPU;gI-TnNEeM|fMws$!Q2Z-y0e81E#7QT7*ZA{(| zfADUsv;H<{rmmw5&my}o{cL}A4(DN+XhZy3Dk;WA7rzFCbKQl(xrAXb(MYbCFj?7% z_@v0x*o>I0jO2{;oP>hBqW>oho)}k|kYAG>Uy#_;+|t_C-qG3B=1TwJf5PDX2nb?u zZ2W`k`{|k4Hs|MyA3rIBLL5O%QlO8@>z`myoX^7FJuEkp*`cvWT4WglIFJ}k?8^+)!)%O16+lJy5#%-yQiKe0JE?NvjS4k; z7^1^kXe6Fd!0McewFJ;*o~f;Z7&FyiHDTnWIxcPo!PC7|j}b$qpb-!e?pYa0nJWqO z+5rov7$o>j3(<~>>fcg}Bqc&)dGO9(?@y6hX&d&Tlzult<~{%=T2%x`qgIo_)^dLC1TdPZDm1;U zUJ?j6?JTB&&LiYlPnHsGQU;=ruqMFNI5#lV=nBoLf-;;#z_>Pc^F5FV9FU1N4ud8m z8fxG`{J@Hs0Y{Bcwiyl|3~=(dNW>5o3L;7lLw6A5diVF+C!yZZ$LyEHJjmHEjZ?aV zc%+4#>XtKBG=$#6(;zm#*~exP6yeHb!k@nyLk_ak49XcD)-D)+qPo9)b)rn*Ex`JN zrs)Y4wU8nPtxWU6(Mug6xda5!zu;Pez#+~!7!Y!d8}&e%Ob(Jse$xw$LwZ?)n5A(= zxCn&BF&63oaJNp%9Kw{5LPF{FgC3Pos(8F;D(@pjKU-Ta?eGD14w3;?u5ARkh&}q~8!2S_xCm~S;MRpAikj=1 zsJt%$0`m7B{rxT5+wNH+?>`o%^XybkYhh}Z?R@@26;Y4KfA?RmFF==Wz~oY>h97l{ zw=t7JvWAAtj|#n-z@#ZCnR2znmRa|H9<<}&oKg`srL{8Y;bBD4*3&c znCt>;D%cJL7m#JHB042fZ2?fC#~|!TOSD>p0E}*RnG?!41rVgJ)sYh!LZqApr4?aIf#Qp1IUaTR<0d7&?4JWg!IgL@7m6HU&sZ!=Y zeuZLqr8glT(HB=y#Gqhd#y4@_841U`lZu3?_8+m})b49)G;OD(50!C>kB>hN&`&Ko zvf*Jp7}J+uNUM@7zsuTA7Wiin#1+G~yCb<)(v>NtDfu`JPO3_S=iLyYs91gCGnkH$&jSQ>dX@T+zQZP*%0FWAU3vn3t0gontsC z4=XK=BTd3X6Hy(b)|FqTHAL(aBRFRxFU^X>^g6IqVzdV7%MK68wQxv7G(jz0Wm3?C zH8WJZLcrQ4YVW|<7ypTW;pg$jgKh~h@_|J7a!SJ-M35OUMUXmeKI95uB543T#w{2C zc>shvNGzm0oK*K4LxmsqLW03h+hchk$xp6M2rn(d2Ulr*x*Bk80oVS)R_6WY+m!bJ z^;%_&?aAit5qJ4yzT=~6nE9TLa)DEYP!4+^@IeDBIp7(D)Ctq}RPJz7$xpl%zuhFpCw2&+$R<#la?9sSv-%#>t5B%>8g$^SH?wM! zRsak9>b&Xa{C7I3=@u#c{35fkb5lcU>(zoG*T~BrK5`bh_1iYjuC;@g{-s|a#AqP+ zwBhcqv*xp?huz@h1KbtLQxl675$}Vhou7YQvA8Ge(4D{g=Kcz^=;w`Pr+jzYG=_jH zyE6J8a#O#fgSIK#X-H4|a391sNU}A@w0xRr$fz&asDUYI&YzQGd6ghCMJP%|6AUHK zoG@iwbcBJR=c_8y{te115qDA0UAmm9Hnf(^Hl3Ygb4(;zPgWmzmAw0#4buE2@Y47`J zX4%El&?w%OV^E>&!#;8Z_HQK($WdM>fL7j zPqU;z_dDSC81^^t3=(}HdUx_wJ)Y(V&SKyI!BKxL9_ITJE}yuWs4((GI0DB=ZFP(1aAIO=MW|Eyg+>*9A#`k?p9b=kq z#hbz7mTu;s;ozwL%rsSA(ts0@ZcUeI&EgXpZ7&a88XM{O-}S<>;jU$XPAvkT~QillQ&c;m}8 zbLu&wX{$3i%EQumGvoZ@i=<#SeICO`Q61U2ecvBfA($CDQc|)b-(Keg)8%6QIEX}H z_>easp3mMQ74Rn>6mJ29CmU}f!e1w$0RrNLW$pp|#YD#_O0&_&+tLNQrUm={1>Xtn z4hdPTffNUtk9$q;G>m+EPju?X0jlUc1JgUjb69pGK>v$ZPLX-#CcT#;AasBRNe=l7 zB79w(FR*BYhbmFkFs7Gtt=pOsyK_^|lYMdr)o(thD9z9=oiePDgHxB%gFTIwi^2{Vyv;i92UTA|c5P?0nD+wZ-01b@> zcDQ3+otXC}-}lm^aa;iXS7Q5kfys8@F5v`{?xXfE2B+5Cs~q-zRujSdf{<#1AV*1| zR-{mND8){u)TpyiRYb)b0drZYX``uZD>D2QO5Cf?q{DWv#f>43nqd|NI+H=JBSB9w zX{)=BU&6`h&M^Y)N^uzP1T-C61R=!`oH~?{#!#Ln)Kgo6u@&!Eof}h)`rCLF_ImZf zv!UWQWBx8a>Gn>VDO-NV4l?P62$(ktDusg%ACTa{oUQXljg3VrV_>vCfC2mN@l+8vyXCc3@2RW9_;`Q_o&l?{@|{H*6gilffo_Aa&747h z{&hecn!<{BfTs0rh6v|Ubd=E;zR7qww(x1X#AJmb8mcPN<$nF4xsuT9&=(T(MAIYa zq>qwgu;pWDs608N31@YA1!lFF+|`6BL63*!1c7z~+h0f@oe9_d0JF>+ac}6smkKHv z3-5-u`-G?^$pRl@5KW)0sbtd=cqxx_p@?h-76DV8s{kNit}?YjBrl1xO53?r`F*MfRiwU&i3A>rh5k%Zpl~-M#_k)$KRuJ|0|9w98r?GwqPHATHKI_xK!_-UW2WJfePZ;Y%0`oLc16aFct4Ay zemfUc8E>y5ECna&%1Z%Pf6Wk&walOV>E2AvLzgS3tbX?&uh=>9K6~%MoBx%qc7Z5# zi7PFp+D-|do2%@$#`3&cJ-T!8-+0wf+1~!O5I68_+mYX6@r|^*P_p1+x~XznvL^ew%0i3V_`;tb9Aa zRRtS2ntg)_t%Igl0UtKF{MS94pK)P8HX@ryi%n|RP2#={`j8CJueAD}O%y(h%r%lazAE5=1F%i0m znHNyM*|nGVbazKa_iy~}i*J?%BH#Q0A(F;${2@Xs(z=8EtsE_wESv$A2HyW*JE_RD zKyFJ7X5efVv1<}W&L2HnIbdrP6b+836iU_qwRsj%aq>0`@4p{=JbI&9B>jdGhTnV_ z*e6zD-Tf+=%=V0iwvVi)F8^3@Js|G}c#+Jr#C7Gp z#@e-e;qcJF#M;lr`;$#wh5mp&o)~n0iUVb%ZFi|?LNmj}iFH5<*MChwdk==3T`pfS z3MdqmsVpc5M^?#Q);znkd3Cu2IXjVt_jqH=SG`|6IOL97zp6g`eR!NPxBF5@F7|?M zOY$#!b~d#NFmSJ}H5F26qP@HNH8Gh-{R{dZ5-D}~P9zxd9Vf4hEP z=hNu)jQ_w4hjp3n+;ambmJ?qNzIQttO7 zSBAeg2br@9CPjUj?d|(-lukOm!F|EEDUddSem@gPcwELo+dnu}Ul6XrT1>;l!7nPS z>VJoAH9DFp!H!Yxz6Khmyk8Ek3F-M{w9JSOE0SxvCRQeh6amHCT2E^??Q&}0GvtgO z|8MZ}?W2*{yt=wt(+*U3IgewLG;KIJgu>GR-SFMOjU8m8grYD6=+t9SYy&eCDwayh zSS7MDgqNqX;-E_+y=)+n$T4wPDB$&rbb6T}_{qG)3sTjxK73LbYo#rtY>bjE|27{S zjf#@dB zRQo?U(=S0O$^}!nsJ?nwZyj%D+$pKxV9I6f@w{}Vl`_?=2)pEvCzIr*ykD3UJwya) z%Bn#u0&P}fp^EBBD2Nquj9arGM#4~o>{w!p0yWc`9PRT}t&x%3h(rNc!?=fk=5{&# znfdqr>Ug_9m*v$5xf{il8A@jDsJ=)dUI#?U`zm}hUN@TQz17ik!H%PL3R_`Pj5?F5 z%}AdeM4pGpF~XUggzk^Ilpd=*4na-KNZmI8e`{_GFoiVqAz!j%e3f4+8qRG!lg>&4 zlPV4&ROBUaI5JL%No1(r&{~nM(U;9gn-r}>qj4^|B1K^8SJ4#ui2Mth4Gm7)g9+Rd zA?poc;utzgGQ!u2s&-_D6oWCzYldiU^eQ$6gaiP!Bnu!Xc4CEVzONxq1(FI;^qrI@ z&?SK7bCT^L1Z^2t!?8nI(n=*^S&EDqDe3kcIHE#y&W$5Pt00Dto$fLXuAGoixmFk) z;z*nE>;;-aqq(f~FB5Bk zf%!LIw+D5&g9Z^Dit0lY>LmR`)au%0Ai9@zxDh7LBlj^>;;s8QTW_b$uUv2VS0i?7QZ({!nt+@3)K349BaVz8koE zI;U`|=W%{RfphL42&v>0gaHu>`2RCy-atSQNTNtiBywP$C{M=u4l5H85wU&nKUf)v z8a1R-Zv4kmUP;-H?@LqPAkn@^2p`GHj`naZe)4vCac2(A ztAX79yS?MlOifLl{CIb#9CdbfjtB~3W~6={7IZMlTxIeqFeD=PXIEuqWl-4byxhFo zU5?u)Msaa*XFosP?zyV1`0y;*1O)}%cFCXr{9$T9BqA*QaeDT4QzGa!r8Sr zU{P0Ko|prHNF%*MAdoW2TN{YOdQx`Ae@70D&%kK2&*=_v>! ziOou!_+8aYTl+FLsv(yek|hDrXDmxV-u{`sL(lx*>;KRbfa3w0b+j4{*zmABH!^yx zp?L5Og1uf!Dyd{BmX_OYb!@(PIFU&?hEcPrOf!W?zvkI^Q~6{jv+hLR_?*>nDzWkQ z9rLMDH|Bv}H^OAAe7;n~kqoun=Y=qP+>Ng@kpQb?z)iceE;Y7#vneHMfXGZ_1XWIsZ-cJfycC>|*+U-wR9{o#xC5?3sSu+1K5Va{ zmdR-P)kOA#QrUFig8RFW1NIc`xrC@ zQym5u;hT&>7Yb9q*e{Oas@X5$dvQQRq|oKPMugli0)t6-su*9%oM8H4QXA+Y;-#OG zABlwxM*)c($3=sR_(gw*uW@R$=rEwnshcec3=vvH!gSl$cdw7yqdbplA3X$P!qtEq zrMi!~?)e|iDOUK?_V2WkBKvGJ*L^VPcJD)geLhnWs=i!UGrb2;ay+iwOlPcpVQ+PEt4YPj*o#4yg!x#n%^j>?tadFtsG9Py3TZ)=zm;Hz;Mw zrD4t%%Q9VW4ytsit)C~Esg~->Y>jz@R_&HiLaT4i5xV?!D;>s^t*S|ASG})F>EY8- ztR|cyV@r{|Oe|YaeU|@%p3?C7bib(^P2coU+iwUy+|GQ7zkZ`LX8DH4X3QWT`7xWI zt}5klQ3;*=SWkyqdQ>*;T{80K;8jhCSDtjaG5vV20ztxE7w7LiMf5NsP%<33dC}u#ksM^gRJIa|)(c zZ0Yx7J8kQKK|>=Cx=!LbG(e1Bc+^=wi6*#omnKrY9xYB1O(10dUX5vORhURl40xu) z_B%^MQb<2JA&;Gzn{fPY@tKfrSjx#;J=*xzT{DYg0x?nKdW3>7@DYX?$>=BTeU34>aF&5wJHzZI z5lzS1eMjFl&5Yl!#vR*_-}$ebe!C3w?fG^gB>VNzw=4W*_cqnbz%_<%_-`4q`=kxH zou{B(i4CY~X!c8&ujV%iJl;+yoCCLj&TLr5KnbVdqV|s!vcRjss)v2f|EzL1pUmZF zP`z{c7N-9zZZ3?US_?+gyhU#9H8E*K@dNIfEzWZO z^#+v2)x(akXtUg%c8ClRkzpFUvpC^1ge7avA|12gby^y*`{s;KPxrA!C~r>t?t4Uy zYVVzqCq494(=C{6-aAc~p56)aNCOJ$@MX45f&sV^APOoJel>}uPI&av8G_R8DxfXj zdwT5uorvwr*uw&eO4WSFI8e;TDjoM4WAhhJ*xNqUyl$d*k82Vemn)b0@$Ns#tuI?qWwbo`wk(0$gd0eTL2MjMet zpyJSko(P^$)4X}A?M>a$`(~c!ysVUe16FxV3=$s8JUJVr>4_9y{pW7{p`Fuh z0QkX8ljU}V$i8EU{hRKu60eTsQmOvV+?U_ATvq|cjlTFy0ZVIR+6gX}y_|zjcK5h* z5I9r>s%Xtt!&j6Sq3olg=aQS_VvoXI?q*>92!^Qvr zC}!~ZX&;yar3lfu3MES_I@@LMznZ2Ag|Fb30M@)S?--8#p{H|^LL!sbJxWSD1LCe@ z1ARY^jR%IKyR%{l@*#qAp_Av{pM>CL#L4YoR!SLf4^EXiAQb}avB=K^kfN{x@Zi84 z#{&KsAQJ}O+YX}gG!pF#(q0g<|12Ol2#`u5(EQG4Q3~d<^`mOly&eh^_@pO98-4-N z6_CPQOFaA25-wO}B2X0`ldkt42Y-93KkH{!6*nje4ixDIwbqYNlzPRoWzYH9W*_Om zlj+eqYWG&vQ(_MTK*1vruLT+cne04MPXYxoa1{*Pr5HqQ0Mx+1BR6~m#Y3nt@RwEs zQbl0@E3m7BKp-cGT098Z48)UQ;9_WqAR>qg1BfgFnJPQVBX z0aydAFeTWITOJnXE13GK;8Z|x5kTF{7n*7)hz3%Fr2_OtimwE=CXG>uMxBvj~68pOTQ{dWskir`f+cB>O1^|EqXqJUk`oO&e(Jm%HHY}h% z<&B&h6xNU^V=o|vfp3lq%b`GW*bv&Q`@@xSx=-k zg#fxJR_cMv1o=YR!f1A!B$uuD>|UkqrGIVpPkR%7D)H(;mi{#dzmQaXx;LVdx{D9Ng~SF z%t5@;piRLPxPO&Kw)`+aO^mzWgZkAemjdSD8Ze#{0A{6weA;*XY;p&G&v7~t@U&8} z55@X%C&|qLKI&uZ*^tqr;73{9I0^}QMG(Og?43YIC3qM~mQ2V#JBq+`xw-tQhoUoloff5}ei#GX+P(j)2T@vk^}9qFoHhoGx+(e#$~2wXNPB94yq57GmamOcKIM{)yIHEhc&y7=P@U0ntBi=LMAk$PGWBeA(oEC;s zfq@u+=L=`=8==3^HAjFKTCfCXaw(1?kbi?zZ_3>iBlmhlMDIs5 z!!bzY%x9W{MoAwah6X)f1PnU>*3`uX5q>^P9Y^#5Xmu@gL*;PgZMUB zZJwFp%vgTdWHl;Q|I_loE5+)WC=kP3kRKb!-SXw>b)6q!*&oNMgC9b^IkvwTo}N7F zKH=!BRP9WPv=)Gw7IpZMR)PP16=TZuSuh3-rlgkpbMhoXaHZxX0I5$tX|kxC5CUE} zmxQ8+^g`yV1xd}q_mX@j8N}R;vr&%pvFZ4uYhBOxZu~Erqyh@|?mHq|%~doSXZglj zO@Y=6e2C8`7^$~nwGt9eb`N4dNd5456K(r;r3&yV|FtimS4O{3-KQH?!hw$FQlt3- zP7oN0&Lmb6dF{I}%y3}(;)u@4+q8s(s9+;DeQ>I}#@>B6AiQRDw5G0<(I_5xf2=%I zH}*DN%0VyEDM6XBUUiB`u(BrphvnW%)e$wiWuCU~gqDMi?iFiArDA!!C2ibEd5fs^ zukM)=QTqE&XsduF<^2_gX0s5c*}rlXh$oS+zfKQcPRDu-{mZTauh^C{R#6Af(+aZW zI!882Iz;`Li)s2$Jz-WH^xC?jIw%kyU)Z%Eu(lu+Umf(XzzdH)z-2iB-S(u%-Rx^3qd9$GN?<0n3QLcglA-fnX`$6ew9+%PKGf|eX z8_Duy2HaxpjWr1A11{}5axA|qaL=TU8Ri@Sthjf(2r~9ayJ7M_2RyV=3hI4zjoz+?e6&s*!>FSj%M2Ml*K;m z5p4Xb?T~*vAG5Yx8(9JHz(EnOZr2|L#GsaBao0m7ccKP%;cHqM#1%auD1R5 zk?^D*f6lU!wDt#1$N!v89+^qe``Z)0x<4N`ewb}VYmG5^L$CpViW~% zJ0R=ldwBAiBk=~o(oX`Vxj|6@n#N~_&YO}vh*qeE-%*nof0G(E|Kv4LBV%9i>8Mm=};Vm6AK8_SeC&TZ-<*+`hS?3mQaW_ zt07v8o&NA}+LqjH0W#_sfZ7ZZf$*Xo>C4yuvoZ1w=cXZ9@dNQ%MHnKoUb&TqAs0DG z;DtPB;Nz$7cl{j$-tXasNOnVJ_FjG4!^!W5S?q^~><3J`MtpO;$>j8^GLIE17=K(P zN$LPE*f3GsVYYj1^>B&i;()P;z#2WcVqTy`J?ZD7C^X?FN@6PV_t1gxM?Le87Wp4- z-wrzp+&T-!y1xDBo?%M!{Xwe!xk>QHeXWh%sAZ*qgEviZ1k?PhakDC-UaySF^PrCf zK8jD)=zH(<#~(g#F#qbj{h4&gQ1sy7{?h};k)tRO%(&uUUT7zt2}o8je;M-oYKEC1 z-m;Nn8KOFZ)QOjvw!uUGAz#I*3aZ$ zF;l!;)c$r-o(i%?5Cq`PwF=L*d(X942pHXZ6%;NE-?2%um^VV5layTA%> zx??VEo?p_xog1$|v!g!yodQZ1+_Xph@lg2V#q!7Pze{iIWkBzrefNvp^b5P)6Cn>E zCI0f|?9P4ftH@V>LXyBi|6L{hW(p}Zl~xFJp5?Lb+ga+l&YQi8eRrMAa#O-`PW<*V zbX~%{_d2KdCidkYk9XI+sGFwQKdFVLxOJP%ULIw@pPc7cHNXExD{Lkx{2kK0Y}Iwf z{~Fg`yy=;}>BawdJ;>ttunnO7kLC-0V5{KY3d^T$*IJSIqlve_^fUg(H2v-UeLKB= z|P*P@+nsJc!lm+t9cNsaCxs@2u9t`&0DDg zkvtYfsFM=tJ!Z#8648?|GhZgxn5pty4S~OdJj} z^|YJGedvpa{WF!=YZ6p*!hcm&VSSfwo*MbD>-5utfy=s1w!xoCgs|%oOb5&Dx+V8U z;PJ#qo`uqhaCt#y^MJ7k#Bk*9E2Nf#!Z{a31j3{KoQjVF=0aq8rRo0+HEL?{XAYm9Q#R<{= zh+-!v$f;K;Gz7V}e{_~)0Nl_JMie1<-e$G80i{%Y%f#@PEjAFsY=QWuOE~6eqbdrpE#G7xl7Pb`m z$J~FdyxlyIyC>5;sCBWmG>9{zDuSvW^Sw6|WD(V{qAJhIR}g5 z<`h&O)mqSO9u>f{ROpQPPu|!3MD7|pZzl9RCzRUoz&DvpE}PGoY^qR#pPpg`E)vbu z^S$qa<+71T>MsfF)GdE&*LR))9G};lGRTt}G!Gh4=m&J9WI=cYPi6adqgv(B3q$2?U*gF3cz9F++!^m}}$#Y{P$ZMS;k@`W1{3KnkMbaMB<*sQJDUEB$dl=~M&p zlbSvlNkgcR^JBiu6e@2GkP0)Eo}Kb7oIEpgDEXsWw2m_yr7@Q(s}FaKAw<&@*P#mb zY2+XSV~61TVN?F|Qk6N$clso#LLU&|*0=1@#@3X2TtqB~W?Gbqk|9SFgQ#$8FtfQx z(lKlj$(T8-JsM26+DOcQH!iH78zVKu`9c8z3NtKDXlyvH5Ms-tDy=mZdg4()j{@?l zX(dTjUqk+U}9zwKpYPPp2$6dS0@1~7d_8Dwp703|KTnb1JI?<;x(;o0vaI!&6V z*}wrUokGd9+{$0O;y?)795Rvy*@C_^VebYUbYw&L@6J74U(uXRB9ZKtwR4U2Gvhw^ zXuI!a6_6iesQ8N`nQiAM_A#__C9kWtz?Gmz`)!SHJzvkx<4=KbMc~tbGRa6q75CNg zy=V@CzKL=QT8~bkk8{|Eb2pwIN2iN z<;0c`?qz(wKNz}SRo5J4X1Gfms`nC#x~bb7YYKziaw-+70>in)IX*OIh-K6;xO9JS z_a1I*Yk6E?>mi%5c379+#k6XR*N1Z$VxJRru0Qub?G^1RlI*2pmtUOdG#;P;F&k)s zj#g^md^D0Be88kAwz7Bhv}}cJFv{{q@3cNWCi4CeQex#t%xN; zl&mTqh%OXaKK8>jM6g;vAaYw)_DJxMP3xr<~M= zhcj0+5-?HwDIFG2@kGNDO~r{`EWa4%D69I$&YkC5E1Uy#UHOs1(Avx-oQVANeAfv` z*l}GPo>cBEX-->d(6l6iCu-;v!qv)|M?3!droX1(OcOPNPE#0qE#MWxoD%n`wpW1H z-G-l@=oFPCr{Y+t7qFkgR$y}};j}5};J3A!3gKix3F%FE6VlHB;!<2@17aVW-nFY! z3zt`K2F4~W|1Ns^>h3m|Kb}@Te|ePt*4^G^+SkKAr;B$;|IZzWP5RRlt|C^dACQdXS0&SdPAO;FGAEyk@t5xwR{zQ`kE`ztIlvD|32oA^;{C8ycWc2w7GJifclES7REWEfrUQORWz$Z9~{z;K=un!psC#3K|w9Zu}~RA7`0bF zO@t!tNAL_oKYdF-Lw7vI$N1y-l_!J!ERX@zkRsDm^mQsN3vz(NaDdZGfvuYmbfn0Y zHo#jp@G}?R=i^yBsW`y5Js@rmMr)b27_kjByAx4?wVIP5n|HWMCap52dPr9bs57Ksph%`r1qK+x$sCC8X~W%{C>!QSIdiH& zcyv!3-}@nlsIZUB;m;}~496n#k|O)tqA)z2eq)htEn~8y84iP!yn}jOLsF+>u?LJM zK?+-ql#h}3yXSL%(Y8=z@$ijT&355t2j0Q@LTTL+LFoJOTdf>(z}{tCBqgl{ac#U= zDAo}WIjAX}jg+oUEBBhHh|sD?oA@7?L)k=a%S7GLgStE|s#<`@cdg2)53x4UUfXHj z5~T`b3Y`NA^i%S>{Gp;)Dg`V$g10baJj#lJlBo~H!~ zK!*j~m?d3lq>^F*R3>#Q#K}d61(QSC3Cr5kEh00w+Ot#I4h)Z{y0sVNrt$gW_wS;V za}f{tEaQbVB<2LB{SM`tB9s6t8hT+e5bdH?ugM*7QvyP)v;pUTqV_|NxDS%VQZ(*- zErz^m0!hVY6=zgv#)1$GQU^FPt0edNnYWfWx-oDLgy9QZ=v*Q<{Y-KNtD6?2n;)(F z$58jWPWQ$}_ttRs+L83{ly2xT0%_7{syX{dKwXD_IABXz(y-n?S2CoFocV8WLc;o<=Zw6%Ic}wG`Sta_v*t)B* zD8H~>;L|X4cjwUEHFP5--6h?ifH8D;N_T^_2*`ivMp9A*1f(Su0mqNMzw6pZ`(Tde zTF<@iAB8b#9_npA8!8`$;H!wrubWSN88)fpD5Yj@EJG`+Rz;?oyd-b0rTNNO zy~=nGSeWfOc`h`Gv0ALTj@xj0Z`4*Ow6MVMpbc?a{*gDg=0GV~Nd3jVCd99IY?~oG zE#Fcsc``29##xIfayfK+{uQw1Z2={Yh)E>U73@v zyf1j{M_S0g_qu}gBmQEhJ7`_am+y_mN}or~eZ!j#$X?k&$Z@N}!#aB)MXg4dB>v!2 zM*d}4ftl79$+ArmT~H0m*-iZtt>WjG;#@cLOPkoe5AWIcuv;@%6K2ekTULv{uO>H* zEH0T5Q=6y%HCrFY&R#Nj2JR)e=j!d7_VnU!_auI^Oo!z~eyxlX(b*HmoCv**ER&oN zm1_(BfWQsWn2pd%?`@1@qM2gTm&@cU3efV*6ls7ihqN+;u32Ohju(*NN@-K(my3|t zp(aW#q%EahKIP|`(Ku)~fBd}@AUCb{T^77q{cjxIOk~75(bauR*k@N*w2)ZwGO5KO zsq;2*jH>Q@4tJE9=08u~fuQ70jm`JS{C|LgcO$)`ucSUXuh!svv8J}hx3|LfP!HaFj}e?z?`~ij{^GXee|FU za4)Q^N-{UD|5Ic#)@yq&hW+rx_OEk&r{m13tKswQ6xrJ}^^B6A^Q%_o{ef~%9~(Jh zfM!?9kZ|e$bn(81i7cFlGWDE%8n&d5N~a%a`TAj1Aa+jgaHdvZh!Ct__X|Z)9Ei7mMW+q0OfTGuYht#V&th1F|1o3U17C|^BnzTs(0PtdK+ z-Wj?|X?-b?ubYmYLJk#8Fs@@SVqZz0w)pr(r5{>@WR0ty0P}E(_zGDbb#(f1i4T~4 z`|o;cX<@dLDO|}6i0t2{bWNl0HUKD6oYG}dj?=RbyZYTzE7%e?5HL?Skh07A~t@YPqbI75`*(d)%i`T&r1{ z#tWQQAnhMyXLo7-#-vQpYjeCznL<*_+(;)08$#q;=M#dG}@ktyIGrpCVJ3ZJ8Y8KQk-dQ`f^_p6WJa5M< z(#VQF1XL3p8OUfilddzlP6jyNT{)NmR};;dR930zBUfsPBwUUFY~5ubCJ#0Q{w4h} zZAQ4*_i!~O5Iq8Nb_$?Us#GwnCRqT|XdptX5|)kR5lZDxSjs<7$?f&Z4abxZiuZGK z%X5XEe5aM?WsyS@d>nA`ocb(}HESKz*Y`~hj1h|fn1xn6jVj$Df3GBnQfisU-3eFM zJf?6z?V$+o@di@ghU3OnDp9$y5&_jPKw?)QJ!m&X?2Kd6;q|HV-qFl{T$Q;+MW<)N z)~j%yTUG^@>gCaJ9#;}#bgIHp1Vzgk36g`Y^y@Ky)BplPVn*Z?k+s9%61XrHVO0~` z^fR!{CuZsAORJ(qzR!LR*l(+jX62_oz1DvNLI7H}zTtme63#;?5^?#(@o@2BzoVBt zL?p0+{<{VGFylQaMLhn`ao2rer0om`fMm~6EUw_syr2J!)?Ue8{4HvM^mF00%Y65m z88H$q?)Ry<5;t~Xj3T=H=TiKKpYFmQ(>FUv&Ni{T%(fiCaZYw&qMyH&TGntn$n$Zh zp>ll7ESw_ruU!K&oaG!P@*71#$lo%T-DdBD)~T)DoweS~5`MO<=d*7p)(B7`WZi-iPfD(oDTWVi~F2 zVt1UzBi0Acq;XKtV^U*nKK{?3%$;foH6?H>0!j@`WlZr_aBiUB^HF z^*YxUULExzQJzw!OpO3&l?TYHfN01{CT zkTnbSBWdRxn1C0F5pWfA*XB$wo*-Z{KrY}%UyL0RYdc3-{5-;(G}V4+Hg+ppGMm@ohRvr^xs&xk3|{@|{hF?jXqor6|CMC)4U^_|m3m$S2eh@}?q z-Dqx4`HPo~nLoa|0jRJc3vv&!8ufVuoCW!Z)CwB&Mi~;o{u|LRY_EUJ@fK=|%Q~K9 z>O%8$$5wNpaU|^eS%UtGr2`QlIJfbO^HOvK#Il3Q6=JU(fd#jqGb@J<;!-9HTEz+N zs3vfmws{_2vQOs;TQ@Ur?QqN%Nz|{Fi%Pc~J6NzjwcgsO&WrSbP8G zXBRu&a{=2f?tj9tlNG;jJvN+1)ouZZ{Q`-{ha&$^`}|~is@g}3n?tvTsxs+i={vEL z)wXAg&0hE7XPZ4woBuql{lNd0mw47Z9T?ZnKZI-9sgT~xcMXZ83+XTZ<1 zzcRPyJ7bvwKE*G8UA_8*o21uUeO@LnH%3Ua{?}x;-k?I9??&o%Z^*MKTIV!k4O}AY zT!tVvQ9DBwcHP*Bh|MOXwI)g1d*)dBPy|Edo>&WU7;qG3`${%?&OAYu_?d&@*u{@; z{?*RV3KRyU*uhF|e5;{KOMJTeHI!JaWq^S8TpvY14j%?bD|Mwk_M$u;f4nl;i3EYA zF=?g5OhTegaHsq_)_mvckm`J|wm(VdsO|^ULGF5)138{8G3JL(G2A`Wi3vN`Ed2uXzYj3} ze`hq@&@J!~@9F|WW?1#ne%+jEqfM!Doh*@o5>SDvMF}kPSe*-}o$J{nlnq3?x54_* z%5{7vE&jOl4@5&SE*^MQ$`9ej^iLHaujaW!8eNti&9JQNPwEmgf%EzeQrGh;uM_yH z^AhwmDCs*kb~VTn>BZCGKFbCFNzUYo>m}tk8PdZzCEh1bRGoKut{hTpkjnqkjmdW! zT6Vz|d}kR>KvR)6Mktfk0wPe@H4p|@=hz`jRZ7o61hS%OZ3OH@^8+yU)kYzxPP3r2 zhG`;w)7J9n>xC2i<=0}}U9k;eXWhri^WA!|{_m5`nSik=*Uf-`0J7krY6jqw0LGoFGuh_{u3Xfg{0T{-bH;v?NI?Fx|C#KcN* zx@>fG-Jvzr6peOZo?0Zc4U^2e?hmG2nS>jHPL{jM8Ly@Lp`?ggxn-u47;B*tReod{ zvO64K4ZA>*#9yGa^NG^n;%unZSWz3QK?#~(tkQ4R#~C4%P%P|7G8U&MRNR_0N;*L^ zgRL%6pZ=IL)u8!#kRGMarbS1M9goN&O7*OHc0gl6wp~lb+K*w2u=kz^3x3}7s z>;7^P#M3;_C$!AGNjOp(3IBU>Ai!w&6XcgyKotD~#=O~-_qmTlyuEHFuQb^UZBJ{E zr`b=I)?Ts{wluU#ptuxt*lCUF!x+IiD=0K z*{p1?`CPPcM;XD#ZAFF5={E^iWfUI~Y9crV-qn0%3H7yV%G?X-868mKzq;jZSPSo~ z?C?TgaOpU5FXoMggUh(N^aAP^GhIeYwyfOHh5(gDqE1Y$njE*uED=_baAzI$9+!*k zItyxt4_aS`W71y#w8^20K<_WN864+nna*{MS|2hXBh$EC{0EoEUMj~B_sR!{%nHn1 zwN^4!ER9f&=xh9Mx`*7W1K$3PO6l&2sC5K0L?}P#d#W^?XKjLGx<&EAjg4FzK`k`a zx)4aEVk5g# z_m>-AZeYKA-FceJYYPNtwKCxV6lyb~_G@s2dOYSEoS1K!bc>L<8x4~`4+JDJ5VS6S zh^R4srK=K|&2St93{w+BE@xm}`2Z$#8;D5Metsq4=+{8G5!+4*P|i z&%e7P8@J35tD8zDNFL$;{F0o#uPf;B4>h%W7Sj7gtAxDVG}=|&i}kQ^dgh{)#;0Bm z2N+#Yn6FF)lJzsv`vjBvZX*ClU0AnCIWN`ZJjO^3WpVuOUbH^AslWens@Ku?cxd|i9YN|7xxj&AE)m@;50I%gb7@(>L;KSQ?b{5o&+!~K5xM&bVZeL#!A42Vgi%!0|X8=o0m@s5Mq28bCa7 zb#7H?_2_e}L=#z|SxSd#JN=>H~^^42>HX1@zUK+(PeY zq#V}bJsNK%X0teS+k}A=t%68~>*7f()Qk?nr4Xt;X%5TF9^0>sBAA)#bs1F-5jDvI zRlYs7UR0(E{#R8MtZ~4EodryHLmEGq;KO=4;}X6ISGES{fb!QG}tE@mOG>b-{TC$5BQHG9Q*)PFZcwRnE+v;XswY z!%bpXV}PqXJj>`6OXgNamaM6@v^`3kD@;E*wym9UgGGk~#K@_F*w7X?%Mh5c6$FOS zDO9yf8ey4W5Y-wpm2}ib#4x*mw^$l6e_0_B!zz*T-676VDx<=x($PMTRbqBTn-1n= zSHZ7jGsd;#XKyIlnw`wc8g|1PAZ3=mZJ+SqP{l?);PmvhTdo5CuELo^BGp#Im+$V> zNshWW$~L<`Yul_Jk-od+8LBuM^>lB}S=Dmb?14_ZBe4uSSU=zn$%xn=Z!2~7$+SLK z2sBrQ37YJ|cbcSBoo3kdW5~5|oD#O20*wdQGW$} zq&y3>`GMqvXyU1aCa8c#(WHJVq2)+^wRzslRo+p~*Ml{$>woZ0{&;=I`38nS5xBm_ zaSbOqc*9T|!GVaRagAiDjZ||jmURt{fp>Jws`J*ONbJ<-T?}aESukleaS6|eh_aqT zBCslJNw4#UtjBPNktj;+`glCAKmeG74eJQCi*G^n3u$f`^ z#;?vN-|TrmS~V_i!LN<+=gA00Isxy40R&`&H}TvOSGno#v%OX`_&S?U@9X%wujkiuFbxEcgWi|$#UK=RVEENd{LYM*^4!VI z3)hz~Im>oEd-Z;NN&(;%*8FiwCM8oOhD`j_d18CQlRRPuVjn2XpWVeyQ=cO&3<4<5 zeJW(gFXYG1$6F}rYA)?LD;;b;**|<)yq%T?$nHY|h@%q2qw$J%u;FL)VEPH7rT3B0(c+5Cw&{yb0UA0OTGyo-K}>%dc% zu|TH2H8TbT19|JG1w;1+>WgPo{3)9PH38XFF)%KtD5(fJ}3XiiDY1NZ9fNvoY|J zBk1ykc_WkcNouuc0+pS>R`y$@zO8LzQsWU4N~8#_oF+WTZ^~gL^h7d9egUMvd*_OD7~tTDT?-hDGFm*D48g^i4KB*U-SK9e|~&Ty+4z6I09M$8&=|_J4iU8K}j5 z>LHKTFd_oqH{cV0>v!*K$r?hnt)1ZxI01rJwe-tM`0*gOCn*N7law?ChwS3v7TXtq zvl)jH;J-f9FPEb+bFD<5CYVC>jmIX8l{-&!1i1%&O&mLoOY9e_)v>I9CU$N9ptH#! zJF2bBC(BM2m-1DK!H9;equ^$F(eo%4s!Oe2hsV01$3YLPsm8gTfP!t=q^LcRO)+<_ z;3`=U?Pec<+k6f9^$D`V~5jzRG$_O-L^`MY_Ep07ISgMqrXJ#ly_w^U%NbXAnIcDh?|B2cL4ZC*Uo9ks2InVXKSQ=uY-0e} zN)eA)k@A3FRaPU_=WQlRC5H@!ZLE@mQU#8qVGpIUEF-Q@uwd!|j0I5TxgjAa3o6@rwI{gjY6A2>^y2sb{t?-@Ja8f7jr4uA`) z`Vf346C*owEh*D(&J_cmplUa zp%8l$7pzci*~?C1^o)@AJ>Xm_iLoV#Q5DP-j$=nan8fhpiVye8DPAASO9WYML|0|2 z*xh3_C4JOv5X#(W+5KjM*g3H{OXbvQtR)z`z2~&et)o2wi?-l{((yL}k zI707tj-&4u)x>K=DF6U`Gew_uG_5Sa#s|84@&vpE0AF)k)>`J11BiFSiH$WgyUi$9 z>zBoB2iLdq*R_VKPlmsJSy~#Pei+ERXkD0{*cqR@GmK46elNwiUY0$9JBL{fs{EMk zI*-%hn%3$!zVesm+eeyzDQc#FhTOzpZrj*R!qTzBJ0Bl!pOOKd#V`$Nw5b08n^7@O z-}uQ$1Tyq`Ej>33{N|*x2^EAI1j#W}X;0v)TwfrMpGLJmPEKdsW*~IQPhXZ3L)vPm z@rpOrX>|$8<@H4%$*uZ1)8;?=*UG2?iY;AU;KZvl`>PWOfkaqSQb1Vg#?5kgtD#O3O#G^z>4hYRI;nM$vE zmRuJ})cEnl)|DEDNz4ZvqAg0NaM*ZyZ2r27HmrB>ZH;EjK#?J{qGGB z3r4pyf?`+$k1oydw_+Cp%A$SyjCQd0k6tJUA0 zfmKH1V5?OzR-3L{lZe?Md`yW!1m-@y2!i57;aMD@2O@be24u>PS6b+QpJ{v%ATXQ0 zA|Tm99}OW)YC%HcClioxxBG;;3d|Hv1BEoAGKyaVFsD)}og~P!Xy!TJEQYP2Gx{Gk zuKg&jwcYdxJ#D6E+b1;uNe8pyy^bhDx{iM{&Kiw-_-Bi&ii8fG%BH5WYvnRPG(y86 zmI~)ZIB>he5{PxN1P*{@Tcp(iS)}9ap)k71=4tA{TjuyLM0ep32FycQGz`JSQXSQC zm08J1b5I)o7VpKn3aW7 zYWZSUl4~H)fMXmcvMFI(!4zG5qEV9Gf9&`!%K`-^7<8~u?&jcV`Hw`0 ze{H^@Rgc1>L5;osKV3`gbNA-^?>Jrr%ZBw&Q!7AIO&pyO_go*c%kQ~+doNex{!u(U z;WY$bPY%}0d_6L60OaZqRJ!$t{P+*!-Oao*0X`=8rwGT)x3fg!dtJ@v8uxni=t)?g zE*p*IcXjJMmkVCCefKJO#oYF9(3;ziqUXy2KGruIixl$jHylg81n#6V7XR9M{@f;H zJKV=6bRl^U_iDgqE-383&{2$#T_S<>^<)CM?cbXt+#{tUwO3(R~^J$aam zuRb|=a9*zK_T)AxA}Ih8=OLl|Y2o;RTVQ5v@=T$Y7h@hw08NqqPPZq+e&I60wil#6 zK__0a(k=W$c30?84-m<3DKSoRC^iFYfr!#4_&=g^AcHun#Mj$|;Z(y|dV=ARgSzlQ zGi9vmb0L~xtG_3)3LK!OmmHfI+q=BlLDGR3T&8mt>@Fs9mM+wzqJ@p~cyPkdm@ZFl zxR%hEN!#$Z2KXRk`JHCJQPw*>HSD^A0k}4X#2BPSv+Xit9ZI({Wgw)HAd=6KQ7Gnc zm84BaikqF`qNiv6zmBJ{6cK}0PYpB>!%l8}pRokWf%r`40}I?PRWW}wC5YAlgc(Kv z7SuCl`c*}I6C#0|mKsml+JO+^Hd+NQBs=b(aG*tYPM>CGen7*%XwzF=Pw%VzFH6-D zAKuQ8cQO^O_5rA`^y5%ZOF;^cQ&j1>693@6tdRL)PPil06D^@=MIw`VfOMtmQoE`S zNE=rW05a(^#5Hll!Fbh2h*?NS0r74ZL)RTgVJ$TlN1E8+xbH-42|Nm;Yr0_HmT>Z< z21h>p#7+KRhWH~(Rbo|#x+5%)3c6^GY_!`GzdNJuboXjxbe@U@lHw~_IS7UvB;TPH zF4;*b4T~q1@BK0Hqsc%a^MBo#}Y7w6%H0bZIcF958va3#<857qgTyokA zZW^7~ElcsF$ei(~B58~lgtb^=u}#5VZW9)ix@Sp|7JHo7_ALr?25PQaabD)Q#*)e% zBoGU1m9@uUo1VX+r=1J0nG(%1z;AgAN=$v?Escus-BXOkJJ$pUwnQ>EZb`T_dIz&O zPbfC8q@6tV3ERH6QL~7y9gtf#aAAs23PfSEq@}#puKc?8AkN=DLQN)T_NkemDQbX* zC!F!BQLEDq6oW>O_AN;AOt={Ti?^@7r6Bj8)sJ9Ez>kGuF_ZnAG3E(wO|akGp%U>L z<;uB8c1v=kU6dS3z)On_wvt?9cJ@?zaq*N0vJJAm2`fC=`sIx~sHy>L?EAt8*R9*_ z6a5t7zwLk1Gb+0q{~=;U+a@pZd6!^GNQkFRK*9Edi&VqgS*%8p+>4Wa>1PMC9`|j9 zy%^7dP1irgzm(5^BPWKX(gYf_;e(N#bZ(kY@)s&a&YKC6KOmm@5f}Zsu2H0S*D%dr zsuI7c6B&293tGsGSG#Gjr}r?=y<6y-yxtX;^RWJzzw$A~w#jGLscCPQ9rUhu@oVt! z-emkUOXsHzyD8sTC>l3N2MfOajp^(oW%rcoD439O*)~2<+_7 zz!kNomBJHebtZ!^4eDfl0pKU80-rj$&r%$PA93j;>;{du1_jdD4W4^EGu%_;)J(-1 zqO<77`x~$&Bvwc_ie|eV^eYg_Bn#jo#C)HkNkjGtznF!gm5bk|FjQAcygD>^6S`be zeADp!)v>+L-F$=y-vc**L?0^t<%#YDKpn5kFX#C7RpEgOX-F0R9eEVDXcK<+;k39m zq#=8YY2;71ikD#|fglQd`4B*;x8QFV%*+y!yrQ-cKuUCKSe%)BaQ}tz4d(WfE#{`? z-`~@cHxEC*@y)Nh>5o){#J7h0xgEJ((rXjYFP{iPywBR=Zm*kMe|hgt<1|sYH&sXc z){9DNPVJ*pg|%tJ=5{z9tFCbdu$zB2qkX`YxL8VTumk0HWGs35I8f`v zeF@}S>?Soil;iJf7hDR2;i~8}!Rxj0!^+FmA|x(LVY?#+2quO4XvmIRasMIVUqK=_ zLLw{!BDewa$OCI60)kUUc?*SQ5^T31TT}R;7h??P2qMy;Uz@r=SLaTN*QuN`AUfD9pu#eWV|cF zoz2h5kXK2zk>q>Ia=-fR)-%$`M*$L+Xv6m@P@UFjcIUIpi!}AnVZ2Y1ymNEYbW;~} z%R=;P#PnbM=)bknfAf3}Ih|!7U>Gn63H?OEiFNDCaj>qAbL!2c`~&CjDHHK!7KNZ7 zjUWv$<5g|?8{`lZ=L~&rEWLm*gP0-xb}Q@N1*-%Jo18G4razl5NwA*qoQgkvf#8`S z@qBsOL1W`V%f*t3D;}&2hXRT6ZVuN6NbovA0a$yL5eUr88Dc6W?qwS%zH1?qCo_16 z(My%)n>y3Q?Bh(wHRG(m&4I|+Q7qU{73S$CjkG*>Gz`>x3wfgvMxa-W-$`S3@yo|W_ zCDj{uWkL->f(W>x+fP!`wa5;jY3Our=ij`0qD90F`~lBu7hZ`6a5TjUyJ!3?jz)~y ziS=EHNve5vmm7Dd*e0>UGr+9kr7F7#eV^knlKX6&MCEe+;DLG|8 zT8P$BPqb{9P%G2PiPfXUMdlU?bytFEpnw@ngmI-1{rbJ6NV%!;zR$G)jEdw7J9!F_ zG=%`mYot?FOGjRol*pW9!IDSml4gyuRz;xZ{peUTnW#2EN(KO@VH(cx0K;eC1zuaS z#vDzpBJUTBV#;y)FYyfRWJcPNN#*Ru<+3)Va(G)%m#w?Yp`=?u^e5_Qd3B^aBg>RvUnaU!O8an8m+ zOxWmE{i1X{n+#Wr4>B5Dk0jh*GFf4p1Kt zxP`5@1#5#v-HnBqhei9c#qpBlKy|A8vgVscEt8<-a*MtDcOFvd zGC^I|mILq4qzW-ZT`k=yQ^#~1d*v{DJfz)VjQnr~&?#QLjYp^zmTW9cNW~MgOCeF~ zG9FkD_$ThmYUJyPTDI${0Vj$9km!mw)!9y&*ThNT}Il-lX zNk@GPQ!H6RR)aTz5YqGY-x{%N%C!nZ@$vg_Rn*Yln1Ep`OVHOCtV}h zX<>rD&B{J!(T-;lNs7T;a{NKqm!<96LDY-9wOVTRMr!|BgSd^Sf#kt8Ar>z&qbpEJ zvWgE~E>mM^YfE1SSNJn%f?BYi7xwVy^aN+QOx4l|lv_N{vZ!yTsn`5o|L@lMO`eCC zg~|ryAi=H=8>q70#{b1opCDX!u-%44~M&H}o#AN}XonW)Hko14Y-uq5WC;z_nsb5od z=KlAj^@L`>0!qr~u=@<}l9V{$azn)9;rXy@yon?mmYpNPaM? zMwA&J_EQF>gL)ieG>c?XDng7}dhIjNS18Bl)azV`JFY+JUHmj6B0U%vRZ+9ad~~6? zG77CO&x*~P&37UpG0INJjVkO^)xoga-EmhU-77HBz;dk+W)a!IEl>suL{nuqo+zD> zAnn-#<}EKdtc3hJGIOlt@#jQmeakkC+j64wqa z9ARWaYV%5S<=X0>R(pveDPER{V|!IeOUfEedZBkvV)at?jXmAJ-wO9UWh`%TW#N`YOEcqWHUZ zyYo5;fc>Bew?i8n?kcl=;DI!}_wt2G(rfsU@nLTDe@tC+3ZP+`Tioa3C2z_BzSjyk zcJg{}OX#cQUoRZK8N&AM-+pQq{pzh_<&mO)$jbY#DxuuRv{=TS^2Y|IIelV$%TjC% z>u)(NqG|?52^;$YUhXP))P~6h`h6a!wn?$)@a*u2xy3s#W0}i!uvwZm*PmS-pw%vO z`004I@qAqC-yh@S#1)00+0!ikFl=$;>-?ehfGXtV4wW@@+<8yO!c$fmtt3EroGnRmN}GHHF;5DFyIAk zjH)F;PfHBSYF4-10UGFfr3+O8>?pxmDU-6021<5Hk=VE#yshWk%Hd?J;)QXob|cAz z_)1QQ^oqeWB6@LA1j%>xI5zDLSNk-(iMMQGIWI08N7SP^I45@xFRSLtR1#_A5=83^ z%RR8WU*L$pv6IzfedO=YBmpq;!q0^i6gtq0ZL@_(plrm<&b#FAngM2 z(~FL=rH0dL%(28TkF{V%o4{lXyJO56kNJ?iEA4OXsQ)a-T;f5RWlH@9xg4?*J=Ybw z?=9ZlfsSvEAw7Ek&Vm{(y1b?u_;A5}cQ>l$>!OlsK3_#B0<~3c+q}0J6&^c^&y|^F zsm>X}v}yX`1PWGmXebX^IT*}$T@wjZ@SGcfNoR@XDbs{IEh>qJARN@57g1Sh4>xI$ zCU(vkr|SqaKVylrjyH?jv?X_x@fo6-HpZ>Ku+IXirDSkNOiZ0R;E6}stB2M=8E1HnxW z-EM>Q$J3-W6S!0yW+%zAKS!Sr;&-_((>EPCAMU-_uYy*;8)d2brbRiOYii5R9p$b` zI;xL+hIr7YBFyq?i2x${n4I7|=cqwg2+=$ctU`9K+$k)2PFY7Xg3Fq4*?-8Ad}(-1 z>dX2!%c-&23r07t_MFCa%qyR5+zuAX(R!gBn8b}3_J7svsDJNbzD5Q zi&dOh>M~_uYA6k6-p@k9$|401*NU>r49jydwe_x$Yjwh(=GRbN%J7Y@LfRat5%g*L zu^d*}nS3HlVVUQvuk#1z?ZJL4lhz1Y+>J1J6+psQWIxuhAZTTI5SPb_z`(KEZG16ypF9c_xEb>U+m}ob_IHwuj)CqTN2+}_q=Q5ENA$27QQUEGy3t91o=AFhEO z^?$hGy>G~FKEIo;vJm=Chz8*Jq9D9mR-g6%tC184pQmMNSzTbqoC^5CAcO@D{<-7J zF)mcb8_>UP7}{l&pfT$Slc!+ZAW3~oBv+I%uBoJwhW$~rh_U?!-r#^SywQkD#~zwH z*!Pi7@bzz=Qp{cKu)Y1ySig8$Z3=y?yBFKfD=XG?Xy$+oe$_xqK=Qh%8RZNdFUhN+ zh+1laM0m7`g&2rKOS>$-(-*_ZG?43d(B0|Ud1}i&ymQo5&C0>S9>;!c`ACo;fefwA znksam5b7d1VJGaciy=MI|Op35AAffO7ZTH0D)zpD+u~Uuw<7aAsV$PRVAe! zOj{>mMikx}QHiMvh$JQ5{ZR2{x=lr6!I`5@Xz}f=F(Ws}Z~{N`Ans8N2$t3c7CF}d z#v!9h*%N@mx8Xnp3PE?ysZ0;3BCxSzW?>pH#S3!5S9fNiNcU3D0~H_Ni3z3LWO#Xm zEgo#ul2BhFBp9PK7<%klP04?FZa6tec!??eCnt~&w5l|3E5%mTK>NmN*GT6sNap0Z z$-65Zs_QQ!ShleARwvW6aq63QD|rZ`nUxr*Nm5GwV=~C-> zmdBJM8b#e_mnEI2_#W?Ro~-eKf57m%o=t4|#$H@5<;j=9*JFC!Gp=l#RhmMyVqe0m zg4t~ylX=n~3m+PrrN$wr`rhshK4;gl-?O%`bsndf*hpgaaRCFaaRzpB=Kv*UMX-&M z1d=^f?z!>N%P6#ZF*b*Pe7+?RPahB$y3Zt=SMY4rxr`F834$}JO!9iGBUT&*Qi#(5 z3XkzG*zFoQfGb9kP?_(lrgmW?`N7|erPaS&O7sYs>GPBHQs-jSu!P4W1BodFr#?{j z8w>9`h2S7d7(f6HDn-Knm)P*a#oB;YGsbTvelhdsAng1|xAQnN=2k{TX{w3`V1K1RT5#%9`D_>e|d~$n}p`44sFFS1g zIp1kL%Qqx|d+KPZ7KHhoFCZX;5$2{9)qno}aou!-m4<|HK3}ZvXBpRBuyxT)2Cb!7a!U@03Dm^KwE z!$C+r5N06<-=)O0?HZzl5b@@T&m9f#Ku7cO zTK6Ggmf(aakeRr(u(U(plCD`0nRMq-))?L5^62dp+5>v--}4!Wk>yz-+}mvMv9KUO zNzjGE3dFjn)xR=`C=FvV)yiQ*QfER)?wB!39)PaE_buob!psWeU{i^J3ffoo?nL*- zb~xvpghALLpLoBflO&FvI8u6dzod9v605&X-5=NABSWGAwJ~l|+&h*`AV6>1Z-zeW> zr(OW5K2bU|3ArUvxI>5_7~l*bQH8K#Q`6c-DCp}$OCv^VBItb)qN9c~tV*0wk&~CS zdtyc{gEsy1bR43z+21)%I(3NVgZX@cwEF+&wE%#m(NR|L51Wj3N&BqQQC#HbQgS+ZT(%2r>g ze|D!?MDW=GXaPcVpF#8}$UBEubO8x1zszn(&F^ZWcb#m7V)Y#I#lO$>RDw{JSXgM^M$l+-~CrVpg9 ziHdqEGpCqXx*`kKRJqTUgH0s55Xbb7GZ$&lg`6GXllULP_3yrn8KqCn2gA~(X)Wpa@=Tl` z%D!mx=#V@LBnPK$2dg8G4#^`(^2n0>ijH~|NC?PquGdu_HIhe`L`4R#?_=MiM)JQ7 z$)im2D55;lB9EW$qiMITv`3|+QQtG|sHL%?@&9Qg5LY&;Iim+DX{pbi50_1wJKI_t zp%34Lf1b@&uDn~CnWd+reK>r6eR}$0EC0dH8d53>ktTC8!FzaW1TlRSNZ20K)i2sx z(>lhl7Pua`5K>Z7kX%v7b534vp3j}5f4tlu4wIhGNByrl(i&QD7Zw7URDd+eK6)dY z`yRc6!;jvG`DV=0d%C__I`z*%`Fm}BtLW>#zNHIUy4%XPEkOan z>z$7S9j(_54VgYs4K{HDWpt0|%Ksya9jY%XkZF++;wq<9TN{NKTuLrC z)z+Pn1Q@-L{ZT5@$7DDsY`jO%{zJx7Z3nvXxe~RkCwd`;oaC$;u{@r`4fnd$pWYyF zzwS}D*UV)rM{;$VRH!BV&mk#N_dl7>V^=X{iN9f9t~U{kh>mM%SZ&9hrg1A(j6#zc z>`qkH8PuYi?S7nWOm;VKzC(fW88muYwnt*>rJ~nZl+1c`5@fRIB)Rq{GK8OLzOYqx z{8S>HK%^PcaXeqDonV-h-+8)JV)L$8mbuGit@-8Yhx5E!m(_RA@4jk=_FV3aJ^S2c zVHSLTI9>kqP4R%#%~q8+?Wbb-AN!LYN)JI!t%h~>6Psp*DbMeFy^bz+C*Nqld;9nI z-H+4FPw(FS`v<`0*g|2!oVLQDY&Gz(JlmWmV6XC*&DJg%ijjvWBD5B!?rQI*l)9kz~raS!6ss)K*&kaf+C9>=tO;tqV%}iM%U9EW_;=-X&ZCprD$n_=o~bJq+h}n z+D-^X*n%MdUOyz0^*IRcmW;k zP(%11mGPK}oyvu^MHun6xA>#d5kY5+5bzFOvhN?C5XNDvvjS6x5$`_+QmA1k5Kj zMuUZkGI&h+aiOxvnLx`}H=Xzi+yqN)?1^ugT9Tq7ha@S!t+F%eqD3W}kQ(j_U9}0r zzN@U3#7Z7V$=sBR_gc_#`@lap5f0(;ChB*r?NRs9ba!OAQ)Ec9?Aoadi642N-0(%p zsTrKb$n%#Ps>~WVHPzIF3#RN3M7!0dy}Ok3^0^Q|CZ!hKIFxmosbfVVwUo~I4|lAG z{tBs0vC`-1zJ+Ur2hZA%VPsyuCBVT0B=Fbte^TwG#Juc^tS34VS`?*#X%5AqxVc1I zztSiEM=H$fTB+&RW%N&ul_7O6()vUy_GLJqlTT>dt#wqrqhJ$4R@RaL9y3jZbvm)? z`lZ>7iIx;v@2ltL*U8dE1 zj;_uucNgD&v-P^A?mh!|&%jBu%@45d8c35@WWV`WKBL|bFTZ)mO`0Em2#^`6^zd^{ zFy9I2?ly^baZYepyAU#{FLifu&I(zstm+=9^!E&IRbLaLxOsaA;bQh7iTLx&s0NE_ zbo=4)#7`^H#0S9OV8H>L9J!4+VgjUpew4H9u;Wn9YWMMkzTuxi4Gen^Q!CR&Nj0Pm zD7g~Fw}*7n3;?7U0Q0^+EittIZ0|n>Bf(2>(5D);7tV!9QcE{G%J)5D_(2P4&!)i4 zx7edrQ9ePNdLkr|cO!b)ZNasL`yBWxlSzF(StA1iZwpK#!Q{tzfvMut!Fn1c z1;HH~B0x3T)iZVh@uB{>>E~TC6&R507SPt{@4k!CmkCEX;flsVcOlpNFJ>`5-K~wb zBC+0+q*oEdL}^TJR$tbAzyA2|_P1W}?jz2ud>y`l*-|*|ZzK<3N}Ay43j)j0@2`T*LOjW1|0R@caqC`Sm@CeKCQ!ZJYhH;qF;bu(i~6ohO@nE$(^`3FZige@(wG zc_$d{*Cqb?vca*Dvh!0k^Tj*-Kap)IgN^a9o>z5Ai?oTkB+LvEy6w|LqHJ3s-s*@F zCz#aJ%o2-Xha6^(-{LA55A5RnA-^<-P>^1qQfKSnsB-; zQOuTqGUtdrfFc6ha|H~R4P5ItM`GQJFPqQZC_@w&qFIE*OwjcRRUxF98{>|k6 z?<~Y$>s+26woCuR0FCzjAlT01!F-RPSgAnBpb@FO6l7t~zXL&4F=*U)i-T|ejplwU&M zT1CS4HuTeDoxG!;Q771I7P#`!Qtu~nRmCz8i7W&fdhLMeB8)Z!4UBK1G`A?MMWew$ zgFue_y2rqMdg%2y>Ku+#miYWT_f7?_iR#mg8uaR>OApLL~y zm#xuiKs26;=_|C6k#guG0U>}2eTaeTPf-R)9EG|)36n3<)i&R z$I@Fb#AWovl}W@`7{^zQ#g(YW*Gj}+W}^i#{!LJ@=#k)mOK3EE0Hu~oHXy0v=EfEmYcMWPWsxDw7HVB zMU}M6k$m8kWNd^MriyHpNTBzPbcfhn)p&k%vX-g|pmqv3d5IHW0xgd=5?qMM3i9}O zmtxIf=>ScInxx{drsCbF65ys0QKwNzrolMVsHoF|YQns0QsCFjbZVwdwc#(7(M?o| zF%pR#=*S;I32do}TJ{fqdzDZcoeV=1G7{p z+@{qB#cSPX>QHA@pWjK{{wz7xZ)4By5#>X&B5P;Df#iG->1X z0kiuZXJwe=Wd`SE=jG-0=H;*E<=j)lYvP3Ad=Xw!l&Lt$UZ7Z1U>(}OwZ}?`Bk^Ug zpd+r+jZ@-mcY>SvOOaZjXD!eWVdWbeo5bXk$rQw2;@gQW!(t>>%^YHo0Uc2ef*?#B zKJ)eO@WCD~_=Y*NzTOvYQD;fDy|jZy+ZZsJdC8j1<~(n~g1{}U42sLL40w;??efs= zwStbT>2`qy{Ivy&;$UAuj;AD~G%NOcmDbBACtQfP&_K5NfKw0^Olb{tX~R)E=JaIz zPZ+=?ycN{BL@kN{_jUR9Q@`=$coXbY%&}I+^Q-K~ahV`qIk!}~m}$90T^ScwCR1%3 z!{bZd2$*?+)D^&*YF@`d>3{a)xmE?kQQ6*=o&!fOk3}I~wZM3kpCXk*pc)t#1&kQy z^wkp%JOndf_)34~!hXS8BSX}6P?`R`mQtl-F+hFF;-LKM5Opt0k>YI07<_gxzzg&s z=LL%J0*bXjEUo2Hcr}9RrZ=3~5FO9EV2LoDgnu92bUA};1S&D?bZBdALjj1%FAx0u zOsfUJ2NnZXUG`ybwa%+L&orh)pg}2^atmzuKvwU;m;wD&gZWiId{Q%_Ui^V8^y6B{ zI8DRoTEnE&%iX*%)nz7l29FYou`i(d5SEXhQxS(w7hb8LSHR-C4<-4DGk8S3C8kV2 zR^FiMpmJh!)mMFO+WhmR>AQ=vctE(qa=dMCg?*kx4o&69pn{&DM2*xYy#5w~FM{tR zA`@G%Tl3QIC7U$7TB!Wi4_zzE6>O&a; zEfincM9kX7XxsQ*LrJAuW6*8lzuOVC9Vf!jR;u>#+lU>m4)y*HjY&btQ-8MK5prKT z^vyaArCGG={m(cGjz8mA{_eDZnr&glFJ>zokpq#}p@cY*U?0G79OHyga$ zU%K085{tpLEA&fuOGMX|Lx;vhR|}|HC%<4YmsTu>CgBzR(q&IFZEvb{Z@R1EJhC_C zv>DTbow?SI9>I#mcY0Nyka-0BQv+Sci=i8B45&!aev;o@kkZ86FH6(k{;I$8d4E?y ze@}n^+xmXYm;RyWh3`)L{MiS7jX9lydtOy_S>|*+=DA8~ZI19tCLW|Ryn-{t>*&T# zL`Amviw?u^N#dQCsHGfexRtY>a?%v9zAe0e`;GS9h4j0Od+O1XxBunfWT?D*MwT7J z(F1gqD3Kl{73;fG^{V<^;GLIoF9AN72UMqrzl~b3pcvLd9`p3Gk%54&_Q(QHti+zQ zyBguy0xwQ19GY?s99wA|8ma)w5Y93%vrptl@T=59fy38DfzOJ=SqltGZ&XZ%N4&KH zV=S^i_@h6#HhggV^TC7eqr1#Ur?cwsv<7k{{@b15PUgjFk7fKqr@b+H1y}?bno$^5 zfs(f-8G7)>m#~+@v)~LNH}JMGZ6VX6Kw!xPLi=zfIaU-d0b+IR2z5$VnN=N=s+@LDQ}DdDQq^MX+JR61f0MZFi$ zw#nRBw(~A$Pn`oj38j1&$P_hRMLsIc$c}t`YW3Rs~c{IZ1WIV}eMG0T`dn=sR&!EDq1t`S*x60t2tk*+gNM+yVhQ`R&u}cZE>xbm!|jc+Ts3+ z7U?WM6eIj4^s`yY^RTh>9w1!6RvGXL6Y3H8FemeZP`SL7lfq9RgWY}JX2aoU@t?lM zouWnm#?VB8*$9heXX=#eq@^1|C$~Yxw=PhmmzP9p_#^q}eBgpv z0u*F()7E|E^%9I7cj*(735(>C(IF{lV=9`bKW7K49o1j2Z{M74!x^Sqs`D1JL9t#y zGt73Yqvg4K=qP=Y@-&w!ec$-B;$)bYW2#qjsJ(U;GnR)a6FXR_eY$=LEJ)&hLCIdC zEFGugi}}n~JpvYmzy2}+K=}jJRKs@%_is%-I5gw`8$1HqRwF0X0+Xs zn&#$xqhb{o9W82C!OK$NXuoLN zo#8;+dPwD$W%HFwh8&BS9_P*+7c?DV!@Fzb*+ z%#PCS)(h=A9=>7I+5IKkI4$c;+R~^PS5o3|G`cG~ek@M4rL-5DBZ%<-tt*y4^?Kj<_ceGc2g-`#sN(x<&hTxUy0P-~C1DFBqU2-2n86pyCD#4};1v3Y`{wn7r+2bq z{su=300#*G4ZTi{9BahhmB^r!8-L6}S7#>bGU@#VdCrj(q;V`qa0z>D?~$@!-&B7( zX&76(T>gDI)pyDC{gP4p;p%5YlL2l`_7dm2!>n}9sJ~xEP9)at+(X|d3VLW^2KWvg z@AMe}wxuRH7C%)=o!sk#C>O)Nw*21bBmDOK`vlMVwZ_vM?agZg?HddoTEBT;SN1dD zL8+x!&imZzyXL8@^ve61rQ4=ux6Plw1b^P&>?kGuOqK|c{{A!Q&FvL%WfSjvmdiS% zYRU0D)U{cjgFYSYm3dzi=1-ZP@UEJ1JT3XzeJ^c)_P;yd!27(E`-;l@qEGjk-|tKR z^GLF~fAgvDBs%TQr{!4it(o|#AMf{ouv_k^u`1U}c@)T#YWh%&i)uI|S%C{nWzo~{ zKy3YLb%|b;a%bw__RPa(p84YF_o_Tzguvo=b~1^-g6{@|zkhwrKi0O1j<8yppnIhW z$1}aknI?yBM^J0oha?(d%RlHeyRl@rO7S&%-d;VMPJD*rD81r2zNJ0Sg%8IN z0WsyPpXPCMV2v`RBnG8?{bSuKRUA?jslkbUok4ZX80qkdVUqxqz=g)}%(z13v1Fyd z&>hz4K8MXjYxK>$l~>k$E>*laEAbg8QBi5@djb_E2cC=VU4ayQIROf<4meH>BwFyU z=Ypx7S%u2y?xExbSYfCjgPwKErZi@k0vADjF@;QC`lNL|i8`<>ex7H@ozwis zRSrtr+l&P+{e5WpZ>jj@WE5b7X%>s=PC;O(Jao++Tj^@vmVZ}hhfm-JDtrb}q4>dw z{T}|DC!EguYCC#Fj+~L0L9C(_PQ&42|DGo3h!IF`UfSt%)Tk zSy4!hHOIb9jXfu7e1I*y4HL_I<)t{V5um4Ckwbx{*MPjpAm(ly+dHWjSiWvXh;Kx> zYDymOJU}HCqh_CjDS~WU`lyBGlUg8TX^c40F0(wbRO04vASo3@Fmai7Acjxpfu}+EkTs0Y-Bt9(#qy zS2{tb>x9{)R43Wqgef|eLeqSmfV=E!wkE9nks*=}%#I$tta&DQB5VcEK@+p0>JLu( zhGyb*i<604i(O{kwZju{D{FPhoqw+R`6B#2#p}5=j-QgVNRzaxPLu-M17wq|)omD? zmUq|s64vB?pZ9Q@7P$9*rfpiX{JZme>FvLt(q0%G$^kFT?d#v#Bc4y_w;nsCn(^EstYG{J{&V&y`PtBKzR)gJk-h0N5(9{&I>6UzSaS34|z zm&ucVJXU`Cx-IPXi^{*S`@`yYulYBUpZ&`_&UP-p`=o7tKg={xP5LS1BYDZ~!+fVq zr-(MHx*evIkBMUbGekVZnZKa{B8^4wF>7)}kd0QQVb^?=As5ciskla#h`=HAXKx;4 z9-Az(_x^h*w;^)uQJ}lJh$aCgC8FVC`x}?1l%-$(B6b$B7r# zVK|~%fn8a6^QpNE9r7Ke7e*&}RO4zk>&`~NMr$Ap#=G89HFad8-q`$pqgFHs5{hqW z>?D(#I4aqd(W9i%Q3`uIWWB0NV(Dq9NVQ*BBNEhfJ6JG@a7sSFvuDAVvO2AnpX<<= zD^9Nj5APE8@!=BFdnB()?xGb#ARWOpTo6AO>0+M>AgI7I5Zy|wbb5e;r;V+FrD{Vp z0y>eH1%wFy5fY@YP@s8(!9n;WL1-eG|BL>hBX^WO>BKgZKjc;@&0Rgk()%nI%I(R1 zi6YkZf+paCa=yeR2lQ1;Xd9-*ul1L4&Vl42B9eJ6_8cuz_a-_TFiwc=hwfJF@CtC)NgP7nkKs~E$J8~QMU$RDjH zvh!)rZk%<(r9`?K+pGQRhuDI>K)07%r7F~VPY8`=bba^}Ol4S?iu3zSO!j~-HXsiK z+4y?HM^b(gprDaD7=Nq;(x^Tqy4+F*8!OQ*-X6ktfj>l0CRX86j)kX~FGh+Y*n|*m zbLF-IsqIqASP9eNW_qex6J_mf2O5WxMoenY82KU@haPKfW>Mr@x}KxI3;^9yV9t2_ zR|XwS6p(y_4!B>Zx(%)ll}p5v)KPv?_hg7uDu7%m9;fPZsNQlGHE{uOqE)9QlN4^l zTDX>=f*#<*KjdK-vH>AMO%SmdMN)JLph*pBpL_zm7QpZP7R}$b_l5fx1NYmTzh9)u z#Z58*)ezBCWNG@BiWG^Pt_H}<%c{EGI$h7M4pAf*@szanaTP8xVwB4LrOi)RRt=3k zt(Bu%cX`pY_o=fyeqnzXW(ots%A-}$q8okqai<+IgWODFkFsO*Lh zQR0QrmG~dWEN3}4y11@rBpX!G#H@$c0x5%@J zJrFL)3&>Dt-|%C1l-kq^@G~Q#E8(!aJB4Mi+dbQic+BUC}A;s1&CIsDSck3>H0V`UAu1g|833j z&Ya!Yx4F@_w(@o>B0_Qn``2|uvMEU7p>Sx!(KrB0^e$s$1z`Z0%bRH$XLl8>C}$ou z#3b~5Eya7>;Z^C;mUTM|&)EW5T32BbSS~!Ci*lD!1YTP=qY@y$lavcw3uu7FeFdLIY5*n|Yfj$IfO zE+Us@w*$NTHz!q}%-Y?dc)S-R-v z@PlTRhjV40CtqMT3CY{ivma2{GA&8aAOV4KkAOCckgzjXnO7^HEq``?{Cn>2`RZ%) z_l_4|`&2`iK>S;KN&UoX^9kZ7lcIlvX(=~Qy62Zox-6jY*hu;sq8BfL(Ku8kL-=?l zwv_w`Y>^8ju2?e`HC7c6wsJI)r32WWM_?U2d;RtD?SFr7|EpPTB2O=e(ny3~XLQGX zNiS>_@}w=M8)Lxsl{*BA)o_bn1mo8h6lLn9(4tWi0$9Fl(n&|@rK(sLAq9)R$qz4{ zzOfsEjdtZ86%g1~0`%Flbl!S$qrB$Y-j9fM=gOVzKJArkhx15MNQ^O!1 zMB?rcLkEpuVGHXjipVR1Sc43ZnNA+jxf?mGEApPHqSH%PN%aSd#R&1it9(8z2A|(T z_!Drrs@kBx0@L4(QJaN-))-@;Y@e)q%(P6fzxLudl6DWqNqxn8w`A-PHGY`zQ zLD^-X*~y;v)6NH)6%hD3p((DC-4v1V3c+VT(ghgd9UcYi#f{-hN~BV^NfyL&MSvt^ zHnI6Ru-_C#RT2(EB;h*&D$Hxa*YBG5-nFpM zl|&7_F-@Ta$RJyCnQRKZHDKJ4yz1p#CL%>iu2_!|sV8>;sMAMa@W(HVN9Q} z0Ei}(iL?;d#f6R}QBR1=qQ;;GOi>gPzy)E_r$^zD>ye+(R8L957i%~cc2nstQj6vq z9+LH^7wo5}d^D3}*q@nHXCKFva`%q27s)x0JD4J)>lb3hbNr=?(Oq)H*Lv(CCPEsR}KG$J6qgZP|(^FttG>qpm z;$|!|IU){J1BxWYau6EPQ(Dyb$6Enud6Uk}2XiMlZ2uE;&^$Id?9(ykBx%TyooAa=$SY`@Q6K zzNA1{sirTp>O;P}2PVG@5Acu7iz0_OqZA~tG*%}fYm{;j7#A@U(7+qjN&BrOt1LB(1KB2;hb)bPzAK~k1g;07?h;ZYBYym|IE^nLLH0}#*GxBJ2}+HY;3n3| zyEa9U_29;PWzx;2QW~SC>(j@x)~o)$-BVADF(!S8r+$%kZk%ecvG8&u#oc1TezgOnWk6q=E3I10M>iFC z<7kqoQN33y5&wZ_VbM$?3-anwOxDb)0iBCPkafx;@E|&s6B>8k(#hbA!r}m3GgmQ~ zq{UiSeTxr|Ls8H&CbG2v$h3g8E4c7gQU+tN0}WX=Z8~Txyf4mT<^A^Jpbh=Z_A((d z_nw!AiV%M#;)(=|S_kP1vgFZQvQ&CzW?QHV+4DmKqO~wge1JzUb0N8jS9_?@)VL+ z{v|ivm{}|Y>WEKGN|LYtn%R(#Uu>SxOgWwY=eEVtzU4O?3+&eGaH52ja>LiqxE9^X z5PKnK1J7WC3FOwk^GD(6kJ@y2veAX|O3DIU5mY}85D>?E-0G(=B+GWUyF<^*55&Du z7`3id0sY0G?89I_deG~9hX>^h$FDR;Uzl8~9DxT6sH($oZ1PQ>jM0oeu)~0_-BDN1 zUiCy01vhcrPk>@Bm`I?_Ec>W1&nd}4UU;XaJMC!f_$b{YJW1I($Rsm?m=jRw=nl}m9R~j%>=?tx@!tn~UqdXJ zITX7wyIDav<*a080@;O2M5HP~YzdWqucoS6o!``&-f=(ep1bOYf^v>a*!I}|(T@V$T$%|1G#{Wx+?%9X4 zXACODb`?@&QKWGD)1UkU?M`vYxZ_2;LSA0cASE&h~yHdX*c@_7SxwVt#$YB^A zZWp5JFg)?SUz`DvBxndd|0~z~TC`l=)$3cs=6)sh3P{ry&AZZzQF8y*>e#Dv6!6-X z!^7!2TWYLUjmc0l&I~y>KzTAF{hI&*ayzj^-|d!F_m@HzK8MFDDp8h?>;)ao!-41T zP_wp|*??tsp!fwy<)T&7YfJ9}+tCZ#YIGmNerP;3$TY5?^J4S-d)t~9?*`Z60bVE@ zdDs%oYwYj*89Ra+>1;KWgE~%9WWJ?2Il1AqENz<%1W2Q1kdY`dT1#2*A!VFb8; zI>!EVI)w2($?3Jo9|&N8fE3K8I^Hh5*@goZN$`YC+lr%v;w)(y$$F|qg*f|Pn#H{d z%n1y9sJ;#Ez76?!`)c_%^zb(9_V)F6XdBZl(XN27OCfyfX+-tyX%z#>zif5wpU*d4 z&5L&4wu8B6LA;3YdadEq@MpSwDMP;%7e6=k))q`_U1h#WhgzLZDAZHK2Q$^Mjv?i0 z&yM=p$lX+mo#ebVp4M33R~?xWUdmX zkrcGgGMCSqKDqt-O}4JeLMjnO16Y#5q$^xRB+tLc*xVg^2i04<>w-U5eG9H%y}tbV z_E7daDPBSG0(vkaZp=7s#wiUZq0Rr0iItm1gCuwg=R1oh(3SMf>JDX%Gj81&yxC%X zAV#?%$H0wEFoS=JBMFn|OFmY&X5B5np$YD!qxfrlYhXIJ3MpfimNfd~rU=wF9Qmh5 z{)tNNvM3ej1v=9`Qug10*Zy~}gTROV*oT9Zhr^tQlj_$$pzIaIeK#aKCF>xDG4M_^ z?25r`7qKd0zRH_2zCZq(KW7G3axx(BV}1C|m6zZrs$GwHlFhdOyL`Sb5E{a7yLYiY zMw$dk#=9Ha9{ZR`#=w$YV(e}EK7m@)V>EY;`C|$#Z~U1A8OwMomtLFV@zvf3HgdDf zT*1hLu|ih+rH1fQr6)qK1PU*PcdN8=McswmPp&x^YILi0!=%uF#0Jy-bLSb=D{Zi%;lAw#90jVp*MYmH5Br@)Fk{=XFtWG!)hNkc+F%}iMYjBk0>5!w-efj+dsSK_isYmE79*72Nd%YU z1rhhj-V5+yK&E^=L&p|h!p+(%{P5fMG zFpT0mTPRK7GG*!X6_sku5VTnUK^@Vd*gfwxJL(mMdEoK^!WxDkV$gghNRyXhu4$5U z${HBSFl!E)W{?2U4CD=QM=*;&*+AewGm9c!pi}zWv0$JVcC%n&)+oAU_Nr0Oj-oB7 zmf38qgm0)j4l%%X!}keT(;!B78O;tK*zUqlc}nm=9=mwLY`5R#R&S6Rd&vtA*A zPVkcLT6M`DSNM0jF$|8bR|G5*j?G*%bYU^8!~rDml`}4bWnoCERw5Fmf~0i~nz=lF z=!Jw*boY8U*e;}heSgNBGtk+>&m*Kr@(>egDT<> z&1;zP>(ueclcffxm=z46P-eYNG_PPH?v$#R5ZErxl|>*VEs7wpT0uxKZfp>lT? zEhnN&QpBOmU-u%xN572r#;N5*Qf5OfL+qx(5h=bn7hbAfY$(VwN`fn>wA8{kA6fmu z{6yg(D6X31ic>qhFY^tk^P!rQ(?usXU%#BephlCmPWMfpeyy%(ong_5L4EvUqf6&= zJzcIBoxk*2U{C6#I^(abNk%&6y=?toecqsWKhi$m*%UBXUp%YEmM4}_MefROzMWs8 zvXiCmjrEkCynf}qP**Fd&)MKv-SX2$gJ5@yT*6(vPrb0E=eWKgy0uho7~@YSjaMYK zXO&H-X{k!Q#_k9ke6F$iuGY%^Wmji*lU%Wc=7}qAhfQi@3DT~1LMCI}#EWjiR9I)s z4>h0%s8j2p0`cPtnXu(lzN~2fQq09&$r4k7a##k(iKVA}|Oh7bVx5IOlLk!93s935Q zkcjaJgGWR-=!|hzXzT*=I3$nkX~>YQLExc)n*snc$B(L24Ayq$*Q&_5mp ztXJ)?9|ku(vQ)Ml!taee8;L(`jhgJX)ew<*L^1?BmgJcS0mYn91bEg!mFkjlxt=Kn zIb4kP5+IDBrLZi`f!rt?gT43dX~3=ZS90qQV97Cag&+)=TwRb<0er@OfuBi8-$|b6ro7=!_eafDnoG z{50OCDie*s2dtUzpZ`8b601&42zyY8iL|tHJhu*~FfL(()r1rBz&mLCV#Y*Ei13-_ z)t)ks4(P6r%-*R)G8vZuU2A})Cacf3_qtEJe@d+saq3iv2^t|y?X_#c*9|O&e|N|f zDZ`B)#SQ)c!oEimmqz`3-S9#D?L~&C;g+z~{fwvF^&9@M9ZLOMvbNWk#8Q8v9>{-_ zYuB$sp1hKEUN0I-&dB@R{BYC0$=3KR#S;QCcZ*xK9ci3A$AA8Jli2FF-1)zUdn*7D z8~`f;JSy`HB_K99n5hKJw*@9D2}i&oV(`R0({=qNDT7aEMNiH5iyr(lfHmPX5AdPB z3JR6bWgH}3kSGkl7)}sVMDUuPa3W`A-LPlRI+K+E`-46a7T#H_p84w2^C6{&jm4<* zC>jj_xfXy7ZloUk*x;KBHMUZ+rrcRn#78IsVaT;O6a+K&&S_GcarT8ELx9(&x5eko z&Qdkw6$CwbpxY?^-cWE6VlB%;E$-8o?!G}0UKszA7%ex!Ktxd6g*zFYfPZ`G0p_K=^wiXY zsB-s@`0#wFy(y7(87(zdX7Cs`Ql|l82N}v2V6YP;v%Ac-jI2m~hz(N2gATt~006ug zhZzf7L*HBteDzGSD)!rYPhu<6YQ;v4VG=4`ykI<`p24+7;GKQ?J&Ph6Fe_ffbkOHP z(KDS-!eKTD@GOGp3>oy2ins$&B`HsSO^nTfbWkiHcA9$_MN~P@wZBl|>n(>!L=f1& zsFf|I?b}{BN?t>3kM99L1tA$&rZMX@3`L8-^HlEcYIVvM{4&!>0!shVBb^S}q{yYJTBGoq}QW1+Czrc#;kEIh_L z23M^N!h+Fs+pY!@z8n0~E3qqLlEGl8XFgmhF_Vv4an=9akN-j9lTwsQm`;&)Y|{LiBoo^vZ)1GgR55`@5XfllWv-xaP$wFuQ>g7v zk|r6>$w7b9p`qLO6{zDe1RjEZe?Z$|WtH_Ck^nxdt7{#6f>ddN+x^7@`*^c&3RfikKht-V|h&d6; z)r7^75FJFVO&RJcTB==dGyb*yF~w4R9p3R^pQt$lAy++!`ZgJ7YXxK|ev-aq97e|F zP+H(_m4J7k^3`d0`)I9)+H?JIY&|23`xn-g@|FV2vfl-7e?aCgs{8U*BO^5764`aB%(So%a;P zSl*>G2HW2hb#-}{%Cv6o&?choqDk2HwBS-Q%N9Z%<2 zyxlj?cC(mg9WQnh=6$c8e71Fuf!*0LK!<54VkfV*-#V0}cx+vAEMu}yd$1eFqPQfv z4xxp1%$Cfy+m6KR+JH{fX$2MG#pA!r6bQ>I1BoekB`?NCd9hX-D6+a|pUbSFs63-h z1w$$Lefhn&gA77|B!}-=9;$sm9D1+0Z^%1-6*~02YD9|KZO%Z6SuijOa|V&duS|!5 zQi)C2HaAh+4MeU(4`-Oi@(tEfNj@g}t>vx9<|c}8&^_}a8)@9&7piF#;iETVkj#9M z4x@knn`GkwkrtzAsu@b<%0ftXZ+6l`gdKz#I8W*Q8?`J^b1Bbpbzl^px)J&H8_XC( zrjM*pN_y2qra1xuEF(5n!wKHG z#-Ra-HnGS|$&E2EF^0P{=A@)-U2NY_ess_qn{Ztl)Pf)yqYXgnS7Cko`qh1BhZoDJ zWkX7J@4V4r4+VcH#6TqEt(9>$VQeW^_1(jqw-NmN0iM0z0DUkVZGkhaM8I?8w0m#b zw&DymC-4A34Vy+xo!O>}nt&>_rY-?TY={J2BG@1i*9;Za6#yH35a5RMNhYnZH)XGa z-n?n=^0oI>jG&c}=WD_>m^p@~5Ce0Ib<@!F4ylfj*C>7%E1ps7QRka|?YaN9MYjXh z*9C})Pq5RN26d`psIHXUY)DZt^Zp+L_Y7mU4#E62VKHacx&mz4Eo{Hf*`9kf%j~e? z_&7L`v1BO1iKlj$9Cicj@ahHdiO+mk-OejGR(fh+whg|EZAss(27k7_UFYt9$T+JI zB6xy&Ac#BM)oR*1x!mh(X_ONd`Got^0aBSBS1E+R z*4yA-?&iMRWg&%5=;2YB!cZ5Cx}VlR@8Z<`)UHwG)qBvYCw@cic74b7K9aOPDvg}O zahZpjsJfC3L)YSj7@eK^Qei=>C^9;$Sf*K$vdv;@2t!rVN2@EggqnG=e+p#t6?z@Z z@LEM*NnZsRgfyEMM_G85Fm;PAU*_BYun`vL0rT3O7AXhe(fuK0po^8d+CP0*!PL-W zVT-U53Pc?YDB~jXf^Ucse3(1yWrCVy;Wi+>)e(q5DDD@fzV zRf7;7`wy|F0i}VSH5pP3A45!8Qf9~k>S(&egoXX8trA3#({~{bGN=dO>_s!B^klKR ztP)lWSm+Dzw-%LjyW@v8Y3UB_SDjY<9d@reJo`Hx`@4Pny8@+q{_F3_dDUa2Zddrb zn?18PTL)fm{v@Lz~>LgL#m24va%%xh~pCMyS-82kneyfHJp z1|TvBAh8(5Que`h5A|uw446B&XRV#~<1QITb~d&Z{%;`6UzZ_hC`^uH;5&NYJ9O~7 zT1In-@3Z@+@4mWpjRy`bqkk(V-e{UM4G>a|g3M8IgJ#Alfw6fHRIcV!CoIErMTEmL zG8_NeSZ|+e>S$G3IeqSq{mCZYBrRF5?J>5THEkHRB+f|R&=4ULlW{y1-JACEjKH;| zEOm8sf>|I)&;6y{wg-i%*7uI1Ws&1kzvDTD&o%$G0^~G>UM8N%TL6TQ!G@fddpLQgxC`KWF!``o*!bLzUN_0Qjhc5 zSRVYcpZ3;z6z6-GHD&kHH7Qy0K0O7=dj21a&u?BijDqz6^t0hWp1~CEA|(nS|1`$A zW74%18!@2z!qz&DjL;G-;ba3J#^$A=i!vULL`FUl%yvRPr0sr4WRUi0fcjLZCb6j% z|3RnktLHFUWhd5>RlR3>;yBggr0bxXz$tcBu_|mASE!Q4X3&@EtXHj_MZ^S%bTOzm zdYo6KF)JE2TiK9MVP(0Qv_D4{zIYXry7nmOeu(B?KF=d`ygXPmTe4M3peyKry0oZS zMN0l-llvq7f8^a~Q&VsJF8UNwfB>OLFCp|Es&qr|RX~aY(u*KXihzdRG4!qh=}iQb zrWl$O>C!=p6af(tktXK1`S0`K%$%9?ct6;A18Xvqb+6p_y1v&(i=rf+{+3m7`_@`- zF1^Fsf&yu`iK1Kdhls+~-AO)~BJniE7KfM8PzU3NPC=cyWcQTSuA-3JHI7kst`-Du zzfI?G0fC}m@h=?@=ey$?Pa6Zi&%bt`?=A@s`MK7UaO-ke{-_~!1k5DTee?J3qY{&6 zl##ITzh}R`&3}0Dl=R>Gm}#(Y`&ZBjg~ET&^$ohu3S&&V-_~k0DUDKe)yF9!?l-7Y zIcy$D4Rc;2Bea3HeKf}87^nu8 zkmlAd^tuP3&s|m;JEA+Ge6jL*LKXP$_9T{^W5zTExfvjEYr9?$ro+(tdy=tvMr4@3 zjum;kp1Phv=xnQSdPI2dx5Xz~Dsr^A0Dqt75S=}KBaS&jmq<$)LEwgd&lO6v&?ec#I~p?}rz)GYnJ!>MNKAcSDZIhP2E#D8C$Q>Bns{-&A);7Rh=D&m#ylm5l za=&_b^o4|AtA|+5Cj%SQ{ED@%Al`p@g zNTuvEqZ3hnI7E}Azr^oRs2fvhl$?9@P&9-=-z6&X@L|sZBWqyQ+dV4Zkd|EFYs;Gu zPQR#0y4%H2-A1&iBT$*}IeF+tcLiC4USikk*Ys0)+RS}@*3p#9ElpYtmm!%kgKVdd z3}qksk!D5SNrx75Et~I0U6oEssiTIZP*y)=mFf|?LC5*4v!h^sLkd`qj7*>F#BFml ziJQAgzII31Oos>$n}mXzy;_?e}c|`htO2mR3fJn#sC$WiEdgQg$|rj ztn&-n*^fowU^Z)_$23(Td3~h}jXbljf2EC@ry80fGsZphyXLFCuR@=1zgISYZLaI> z3;>V=$P5S5+ zGI|U#u~%F6C3fT~AEroKb$<$KQ7)-Ot451Jf5?J=e2TpP{HMBHmde0MnADF&L!j2@ ztc@Aj#%UoV(SRti2f!zQqn^RoZ(2dQ0y(ZO?z!i>CT~%wTn5t~;3YGYy08|M=}4Vg z9G^fbgS~6tM6{MbMJsPz+fGO7G{;R4mIoUUFvmWG(+Yr?bfioeieU=9)_EV?2TI8I zkb{a~ftdqREG%kTLi(`GN4vp;irzHhcmQiTE{JJJ4Mc~Ep_nE>y+1F!^uPh7#|TYK zeq>Yu_yk>{Pj<}ZV#@^AGPsvPTl=;cPoz>2Oq-rZdDrd%sm^a^`D(YVvY2zKT(`AK zeYoVa0suum1px#_*;fctLIJ24P&F2sP_4%4hKF&%%OC=N{d6N;II(loo%98DYBzke zSRxjgj{FI6S&w0zK@md7ddT~4%+WJ{7u}am1foO?=gW8E*k9+<=%Xf+XLGuB#Be;~ zNU#q3A2S7^>pm<$3$7y^bWlHv2Y-3HXuQ2rc6HCl-*A(SX+W2olBLWAxh`CcB}hR> zok~1($}-ISKdYE;?&RD9RHG;u09%3G?zxTP3-2maVrgoLU^HbfXvYX(O|y~6dgMq6 zG6IG}dUhxsu~5dQr~!;05HL~Dz=#*KXCQ6n!8EO@V$Ce*xrYj!YYrb&+1;Z!S_0Ef zV#gqY2Wpr}k@wyCFj-_2l|A9>gj7r0CoPm*LxGpYtHQ$Ck0o{a%R_{_0Dw@V+70m| z-Ldxl;tAI~L5tfi8f7>Ba?$5JIXO+vpN8u5jSYKZ_cO9*a;je2c#%IlS&#APQT zf&?^G=_^5b1SS?b(|+qg6qLCRK>xw$h;(v0?OXOH>bv|4x$aRkbPDax=10q(3+_9p zLkpcyis$TmKiVcfHvaMwPToU;ggpn=9hHO5-Yv7qmVDyR`Gs}V#wF&~m1F$q@(=l? z`KJ`h?l0Gtnl-6*r)6h-7hjr;4KJ^*oL`pvtC;l3W^*jEDDt~gm943L_sQGOc0b-O z>YAP_wD9zmB>o$H{0X>r_I9nmV;+238JmW3>f`HT!1LWLLqddmTf=vm9c*zi+{F9fa{;AG*O3b6Y*8CU*SnQZkTlR@j8-@ zf6$}1wB4g7_2oaz_CJEYXTA?2Q@ zXJzXgfhv#~9YAn?O#FPj{xH^QKCxOre4Ll;wOl?WuTz6e{-=Xb5m{FMJx*Fb#Mq4E zE=e?_Bf#;)I})HGIHgVIX#Pm|(s%sQcJF-u85y@T&sUNN6^QLW zm%z8LSI_r$gHuE4)LxTun+H=Q824r$Cl0SXyu9p7ZZxrQ8x}u!D1%TFUvgXv21%Z7 zTWdN(=?DOGB6P;(i8)M;f*+EN@>WHd3wzM$6M~(pw$6Xeg3M$XCNaW&K&4}P4vbv) zgj+yLv7&jq6aST*I@voSEp-&MO@iU(_7MicdGYeH{pj3dk29-@EZ9*Di;Lq#?f~Ms zN$_as11uT586t`mqC&$w0BlE_?eZYB|*5 z{utnboIlc26C=r6Q^~t?e0V}F{mfYGQNs7TXl>!GZA}enm6o`jSZNha8QI#^{we8g zmm+gP8KXdQGikYdyn+m#5=#ZU#+92)LRv;$1Sp2S7dsR^(WR@y&HOn%mLaDM8T?H+_t?=c6}&HVL> z;W=&u>6E{tBRuJ+(NNOW@bSfNKE!;R;Cy3p1U9+Y%k`l9qrtX&=mp>r1 zrlMh&m@m-!J6G;hPJmX{>jr(T&_X}W!lsaFT618f83*DYN)~oS+Cd(clMl-zYx^fh z+!lbe#4cG#+M0RsH~p0>10tqDnHgb<@lK!-+BsZ{4BFde@Uv>lex?~TnFyN$m{-b( z3j`SmVGP~`trcgJf0(#G>HVRfW@o&H!*g6Bkl|u~iXnXFIJ1d*y$@bJv#0yRnn7g7 z_Mp#;5NvJ8oj=F~;eq5=!95&l%uzFrsp!EyK+Yov9}S210}jAk2Ohln_BCzEK(!`y zUkGwrj7TYGd4D#OU-1prqghX0<`$gc#*eIHpdcrw_OT@kgI7{MD|09suD4oIlLp*JZt1h`q(3{-krXx@ve(h?kFi#g)ki89 znKxUGoPKF=&IVazy|SS711q9Hv2^0EDq#h~0d9T*Ru*|ySw7=ZK0H=F0~uaVG!N(G ze3x^5#4>zRhG~BWi~X6SAMwcUPlhI{*CADl%AImFy?*wU1P2+-I@xvP=uXhk z|D;jWid9~e-mjFAl4}d>c&07AtNti(cr^Qb(!%IzR;*_0Et|GS_u7<5f&8~-!BY(K zH|6AiP6fQUt^DX&$fL86$eW>2A)#??p-E?GTg3Z^MJ^UWK0zcfnBiCBpWuyUbH(GR(UeYfYLegr=hIFI6!j}~H%5nHfK zoQpOf(i#(1gspu97<2T`I1FfSHJn{;wWWZ@Q14{LPyN+y4z5qjLJkl(7cBWP%7(r4 zsg}Eua3vg52~3XbWAJKYT2I65&9zJK1ed<$UkizAI7Oh^@DaTz;~Rn( zzK&%4l5VhRO{_JCcF+TYGa}kKdE06Z2XzuiqxA*)4Fv0oC&X4i3-@>ymiX+yg=Y^_ z?La2@qM;@?fGw88eVi0Tc}ylQoDN<&**$8Ma#y3u?Y!eG-2i-t62wv6)% z*j8;skD|N?Nh@#21dYCERza}tt^+f(#tpO9V%KLj zt{k(Ci;``H=VPwoG}}hcO$nr26MR99ap+_3Gu2X_M=|Nw>jeZ&gfFc_K?%(V#@xz| z*NZd?;kTBIGG`^Aq#jp*Hue1%^VBX2gTl(g!#L=vc$X7U#PR%_FJGkbTw>n@a|{-& zX=L48yaM>e2iXQFv?Y-uFRPqoBBh&>`4(-#wuBa8O^1-0g3g*2o=loNu>VPrmMqz8 zlUrsIXSx%oxq)@>JL`HB>ps}l_dTv3>#UzDte?HCpI2;HysW?Xs$u)G;mc>w#h)*P z6f9of(4?8cNWUgSa8vtYd2RafN$@<@lOG@R4)z;gGcCWqq0}rU)y&=1%<{Kch^<9T zsYM~YrOmfUx#+c)O^f8;4DLlgl}DLkaw&C}1`rByzzT6S6vYHs)22Ht}JD9$nMxI9#*|3 zU-sEAAA(9_%%CM%`!)f^`IjvWFv*Z-vSrZ>AEO1)LFu$$icPA#lZOkju$eKit$w$f z4%zg+({9*v+AAWE&kfTYa#9Dav<`*OH!g%>qkW%OB#VFA7!GE)pS(9@*g zrF{_p4unCNXqdpb%o}tJ81O2Zo*RYQB*1u^A*Sr$<^rgC!eCl^x5vr{TH`m_e}}fs zugEk;6<^ZIPPhKcvVCQ(xBFW+e!VlpHtr!HTx-*44U`>r6n6!B8_@e11^K9nxV|^L z_iGJ9uVuCeZ_IJfqh#3VBJ@dISig1&EV<~H7to0?Fs{wQRWW|BvLy8pd zH>E*ho96h^{H--g`W$0Vph)ElMT}L%fc+`UGZ%TNFn2AM(eT*M@iJdW=F!yrIq0ij z5}MdnK`PWUVQ85cFcjeSseA>VS`U(EiFBk9#lfV+mIZ$d{C!78w{FTJ{>I~V>u$itcR%hrxtdz5OUSQQ-Wlh??r@FIDw_P-)LmlQvqb@?4j}`yp zOFqwp6=QdhbF8%cE0zkVYl&Qu$jH*j^R%SKJ-*ESNz?Aip&Zhggl8WPROI*X{F0-o zT61^iN1wrU9j70$W=i$PEJ>8GbbC!@rq3PhN1$4AJp9pp_=|M-^9c-b|HnVlPvBE% zJw~3$xjLYFM0@Mk4F`3o12%gxlIhkl-_zqh_?y9M+nd?PvQK|2aNT0%62+=usXzSI z`*339a8zUYhU&i)n-72PtqG}Kx2Y8d_YESIiKqVmoj$sC7Wm(v$p6kh@SMIcI|B2r zmb$}-1z14 zYV6j(Pf!0%z4$l#;op~Aq^YIv694`9+;>hJ`?u=_C3z|J@xNEVGUH4tPJJ0kYta2( z7U@EUpmxlC$|MGQTT#i^{i!MJ(vR!UX?cb+&<63!dUO@T*`jwkEy!E8b>d;PbMFd} z0=;RlN0+0X8KwWPL-I1lQ9km29Fpo(irD;Q*}Bha23?Q;d7XSLkd7TH6iYd+Uut$< zW(EFlhlG)G$%wh?2U3Ilr>e`)YfT^mBICQ_5vrZe6eoLKCpt9M&zR2mE)=-nw?0;^ z6iz}Yw(d@qYe}Ejf!^%LGJ3RHQz^-Q>}d7o;n2j^`I4%+129We#eh4@{atT8Q_Lc%pS36qGsvD_SD!Z8z2QszVKr;Xv@gdVM;}d`3qYfp z&;e|riD;k5BnwmWu!2WWIMy`?=_vmm1$H5q4~T-KpRa46nU2;q5Yh}=F>sZ0yeYM; zHGr2Yd~#ivO4+4+(!>Vz#x>9O$uM8OeZ1aIzFTrlMXZ==f2Ep_y2~UZvb*0rSZGxt zJC4n5wkST_n2-JWWE95AHomcfG0CSy(1Y|+&nFmfJw)~TYjUuaEk27f5f7#@lK;Ad z$ho*K^;F%$Xi()>toKo3Pp^m2uJe=Fd-6fUs`Kvmvv*;9cUG^35T|^tP91pC)K7aSRdRsUAzN~g z&E&v;2&J@w*7!#i8ZZKo^8>}sth(;?@7pi`u!x*vwd3W7rB+tPbFtx9{=~F?cMkEs{+`>TR5$;YL1~y(85PAq?Fy# zgj^-Y&fLv{I*%DI#)b8_g^gyKCEUR-v5g<7!w;GRWquw+r5XSD)~}NOdn~Ju&~m7E z-&mYmEpI6ITd%}K#P9K2G%CsO=8HFe+G*XgnAoI?)@B0wQNNsAcxLkCV&}z&-zQ&A zKmOS2aemgZ6K4)2a=^}0uR3-oaz48-tIb5SF_#I@?-lCWI`aINF^YrqXQ~yy&0^)~ zk21m#AZ+JEguQ7D^9li`1ShFeCm+S|Ah^j*JbICe4cwf5aIk}X0aGP)Jgojc>}-(X zyVkg{Vm&nLCX%v^U5QyE3869>F*xgTq&~d+gp|rdrlUj9FqlBNL~^21x7rRt!KzvuKJz z6y$p#5ttapEv&c1WTMdR$=JF90Z4v94<)M~(Dnu~873UX9gSmWUoln(f4%;jwf^QO zwuQ|5yy6*S6D2&QJ>d3F#uKLkG!xnpyluaPQqYU$SVCriNCQhxm3FAH?1o zOJ9pm;M>|jLKf6@Bd?D+9Y511tVikn#)BocN+~FZG$@X*Rs_jp-6ii61tsu4Yo^v(Hj4w}7gKz9Fp#-J*)zxuiqL*%@b+Wb<1&BOi7Gg&RQ zHv~O$tZf%Nty+2?H3l?%xBIa__wKEw96!B{d-HElj3TAEOLUR?@9d&AJ-UFODV)ft zc&IVSjSj9Wlb5t(j!WMuwGkWApc2Q#C=)E8+Dr4GT3nQ}BLFNduZB3D#3?s9Qc4kP zP>gsSv;0>wwVY2{9u}aoLIdraNmgzL)3FtbXee3`kcF+F)v2yZx&s;2#Nb$1WnM${ zE@`3VDcY=~?EMa4xj8AIM2zx$18|Uoo9eP4!Bg)K)77a!$hOXGHP2 zcIyhCojY1Ft{Z+Vd*s^7P4N6_|I?#WDnEt$!7X4NDUVxS52q?mTeq!z9yt7USxhflM^ea(CgGGt%op!r ztiG`zLN%!WacCBuEvUi(01FH!H-Rv`DU{)Hhc3He-Gx>|{)p6_x}3U~TGts{<-2h( zg4jnh`i%R9OViUB4k(%H-f=HR2h4>IDjc$0oxYK77@zHXoyD5CAVmD$ZTj5%?Kk~k z^HmEhH~@!@Qa?;UnbJf#j&e#0&g2|kN@w90P6NbxjRD`@inaY#y&O4$F_GyP5^ zJqZ8guQ)kdILEJy^>;H8H?g&*YmdLTX=fV_{Ib={IHGbG^rB1mar9|hg33?Vguh=t zji0^gI{F#@!f_4uDx`>v^C;frDjI1{Brbo6@_W&>4S9$UTDu6*da=?~kgy)WV*9OJ zf^wo_)uaoxL8BwD&g}GAYq7eeK2j8PgOdO4WJGZWDZ=F;$0m!!desx@t$&dpQ@F0u zT7G+$wZKEYAceoZy7%#2mp_}SAC^x<{N*{moKX1uxTvUM^e~ z57^j}Ksy4Z@Bk((m>CaNLW2*@z$$>)H7#LTe7O7ZgQONw%uD)e+BmRO+_#T>=Am&G z`Egd&Nb6lIA0BS&GJ3)J>&`i_9V*^c-H*gS7%(%h)E)KcMVoS|yG?GK+3-fB>O2d7 zX!KK4ez$o)@phx}KSHJGds=1826A9><@C1XHcJlMve!P|YN* z;!h?pL?%3MsJ!9TU=G$h2HiR4p~P^x#~N+kH{SXM$~o7f=mSd;TpB^H0`8Ix3$9jH z5uT^UJIp4#%!>PPx9xmfxUB!1+wwW;PLqRfeQoNZRT>iWVhe-Hj|dO3w-fSapghds zu2!HYO&~$*fC8AeSg&?9uNRs~da^Prj3$sxOat7d4L+V~6@BHgbmCMckSVOg^M}wM z1>M8IegT30q?~llwsa|Sw^e>gxnY#NURJCHeI#Fq_;40^X-q>5tAKQTi?_m1Rm{7OEz&IA9+7j<}2(*P}r>+(fC2X*55fhcR&xe;2i?mLNF=M?OVhfVx-^5 zshwQ6?du7~P@zHBz_2`_f;?||p2sy--tx3EQSvd15B%K&lB%LfuD-KnrusE(SNUS3 z1!Z}^1iPDzC~OW?fgyD`Up<4lq@zH)lqE^K5VukAETB;HS3!+up+2y%G^y})62q(E z!ZOdIhA`$Dg~GPsf;YWk*6B4jSgfHK0@%c?*xJ9?dIPWspx|FQR+1@Pji1I~hZ@v8vvI{L-M z|8_|J?Y;PqoWR%wuqJ}7F$9(rAGth5sZ@J!cJXqSg=0u8%i({X~*4NjUzbs#wo4?vQx+X_*?>%|o z;u#SbdZns-TvGJ=>-X!c^|pSvd-v|j%E~oJ(myzQt(2Us?p%u;*QQ5*U;ozl?A77P z%IK^Blx%r_SyNT*>d(bDPWF?--}Q_P^(EzA)QH-EEEyS@Df)^;|_p^k>$Kii%#bvLCGwzaD>`9OsLNb zvokWhV`UGbV|cf}mCe9lL5aA^$}X?1jZKb^NRIfw836e|^4+i1k?;szNjyO<27HUx zP#1GQlL5w}Uaafy)RW9CU1^Q~Rrc{2N+X$Fuep3UOYn9r@36nCPKvm**WHC94Z0-6 zjUCMeeAVa+Yz#$-HLk2T5p(Ork=xXLJWj`RUvu}w;~6KUv2t?ckJqyXn-9Tbot6(4 zTb#yfwG47aY~R>_sx(@D>oeaL&~s{izwN=j*O2q$Kl<{o=5GXCKae8MysTU8U~I-p z7rzz;vR|RyQg0kN4&+EX|EoW5-kna8d~qsb>uWt4?2u40RuH&9mnQu%jeQ|tzVSr} zQ{`W_GrzUKFn4=1Hrb8(9`Cu-B^S@zwaS!-GvXzG6BO3_NGV(<-R~}b?|!axnCO20 z_w4_xj)Wze8s(VM+=9oNngRH8)}txzJz`K_mo1&7BUyS}M}pj{H{#iYcQ+E4v`wEU zZuqyUA=rw~En^_!CmLWK%^PMwL+%_9f=UJzg<^`lsY4McsD%zQ!qIAku}3kf(R`(1 zu_t3o9NY>M!pm>7Q(hs}=tUo#XyA3QGMs9XfH1yLcPpD!I&7^DcbIXs;J4zSInQq; zf3(&&L){L*Z4b8V?cpo4j7s%cLir6DjO|s@AFeQqO zuZNP$dZ|LIXu3`yl=7U2qhgvs#UkYvmavq{E+{ohbwNN3NFpCING;tCjG^%z741O@ zpA+KYj;%)^1f~@LuBR33(rAbAVjj(nI4KQM3M3wZ-qJN4MYkai{7$Pej%+=P67MF} z%R7=$vLs5DY+^K=9LXKEh^a2ALy>3WLqLeTU(SI%KW&b-vhVnY5rvA|zh&Z$Ywpp= z38F3u0PTGb3bddwh*lFdBzX;*o%AAv7?6jWpwgOe*o_Wa5Q1A5r3oODL8{#l^O(fK z1C&2h-ZPfvs3?%@&2^PP2Nf7Zf)E z?4x9<2gZ}B)N=q4t&e+F%#WfuS^;*v21IsiY%G)Jr>K!j9Ne7P{@)wNYY~*#1c1kI zxcI;zC=g=CuY<1eC|2;`SvBuXSg;RHvnwjGwVz2c`ubJvggO!|V}&1EH#R3B)kdvi zCjo4u>mowyDbEVavX2h1)U+rfiZxM;i-?O{*k40LOoHewN~75Pz_by>4=mTG0|-13 zac~p`VF1|v3hxASJ00DMD)2c+1wzGPx;81a%GlMe_M<;A_?*; zb8hXJIVMuYz%Sg8NMpaC>snfBj|gI;ysbN8UU-~cgnnk;td6emJ1~htb7;m6=yYRa zh2Afw(R7$I@X}SsJCKjkanDxrk)iafG&!E85=1N%e-})qR)+1$Ch)gTi{{jqC;PH| zpdtI1_hz}mFG!Ciom?Pf+OSG-et10i>$l=J(p6T5P{y6qO6gG2FN%Q7&Fis%-!A}E z)mWJRWHjpu0d7>S&NMO^uXs|5Osm!u(4R`SJt-p}NL3je#;TIGWUD;Wb#?ToGYe13 z1&wO-?~F|6cAivV(rOId^*$ z#ssr8CkwfXk!X5L*;;{Veyg^T**dR3b@o)=Rs&13uM2bP996t+KaI@2>CCBrXen~{ zoACVgFla+anuz`P&iU>qmane+WgKT&t-DE&nx1HhI#L!{_X!6#Xjs;|&^tm#8`4N%FMIkCT(Ee9uIRj9uyG<@V`UxI z(%!>k{5sX$aFg}b`2gS)ljwi7#yo#Mh@}pQYG`ugOgTj;F1*fwh;H#KT?|*g`<1Ej z{i~2P%ZN3U2EjX7DapaDJRxjCuZiVE6ZQ;uY1K%@&7v+sCN_gs-BLv_>RbHuupq%q z0K)CO1d3S)s5>&Y%`8My!8d%x&ZCgp!2qel@8evUAgJ|ZHx*lVg z@c3h~oK*4VU0XUXwjr_ID00IuAd578AOc|0XnMo@w#HJFCh~esTL7I_q(NqlEuqIl zJew-l+3y|6%p7G`(+@=-X`Axjs80f5@kCcNG8!(27g;CY(5&eL04~yaYG|(@pG5Aa zKRJqi+9eF-_rSjX`Og_G%?5w|w)s6t;rUk@Cv~wu$@TAs`W5|@|*SlM) z$93WVz6-4O4yvs>xP@>Wc$joWwXAN?1eWZ3J?mO|IUGzb5IOArqHFc+x%x9{yM6s{ zUF)!?y`4It-`x_FHkc;P6-G+dOh4H8 z!k4@cPd~M(96tIFxc;~M@(%^+$b?iGvDN2&%J-7_gy((u%Dnw)$4|!7@c;fE{C)Cw z;MS9_?hjW|4RxdilRF0wUyzQ^o}BfcJ<&ws&b~#SF+Dk9dU6r+cz@=Eht}~P)(Kv^ zaUr^KVbuw?=kc%Fxb)5)$_AZDd07Y zmy(^}q(QErg+zy@xC)QBD&3?S>$p1IxCZ8=SIlv*6O)^&<64H2-&_X>s*~OcCUwD+ z8uF8S+T)zcxCrj935m{0%v>$=>aDsGRm}gRj?f+@_X(!VR7>^hrcGO?t(^Z4KeDl# z)(%hq5}Lj)n7$>SzMGh~wUGW}H~sri`kqJn>3RCL{IfIov%d?^_J*FFGvfi|c+g(* zCNmyVfQQzkQ0k@IEF^KYJx*MB{Oec3m!ZVE^OX0@39NgG932@G@XSiVOkSHzTxbf{ zB6uj9D{j}(fI0aukOa*-&uD2+%MHyq_Q;gcOJ2;+cyOM<(~+*~nR#iQjlM{cv&qJW z;V<%^H4Jfy**FZJJAGp2QXyuOn4ckPp6M3koFwA66Ep3Fvd1+XV&Whbszr~F7 zy~K=*oGkLx{qs13g7|wK8AW>8&ubD2k_z(la-Uq_Kkg=tFoxZ3<1%OxR|v~QPlKc} zFp13kR~K=rC`iK`h@uJTjPdmmOdF#EJq?Y!Yf<1goN*tX{klENo28grsPI-t-lwEo zKjy;Wq?|5Nb?#zKv6*$MXj0*^TYg;!S6fn^o)DT91vv!+%rcX2q0!cRX@N(Oa02kA zBuo_xWHW^kZPLVKvS&^pM#utj9B7Reh;_mT5+M}psdB?5WS+T>9e7)X7a-fLiNw5D z1*K2JN||c$zr*rgCO!WXS~g}=7Brlj$C9E2%y_EvJgSPTB_t`32tFVOJ57RFut4)B zaVjj30Y&EA2f2&)IWY&YHo=^UA^py&zZ6DEJV11H<}Q zsy6J37R=$5_p`djx#gug3aX3;!EvCwD2UTKSi-NNlMbkdtY3+)yNfMHOI5U8f|Q$T zuDjP`$N-{5i0Y)eAd1X<62R#Vx{E2I!vckfO>KpWcQGWG81A}%t$=$eFHE6v&Zcxn zIQ=mBl@WO(TXN~srH0=_rMC>qzOyzSho(u}wXzo0ybySP>nt;&fXm=WJy#ya-2~&p z)CwWN7#xV72)>IGo7xe-iy;#`0fyr|F@Cjt-%~f_UMu6?O52(MPwh00N%7pfCT%9PYN%a!QiUSFvn%Aa4PFNs-0Ps>OLTA2_#MLR|0D-1$ zbmgxf;k%qL(3sfkEIvT!`WOi1%`5^mAqhj{*+2(EK==qGwXR{GqUdR7$wQ&;obL^m zyZL_>8<@yBi+En7eX8!*ZE<4hs0i=QHIS%ec}}bFs^0y1V@FOP3%*sz*Cx@I*vzXG zn4>ZYQZADUM9Gtn0<7#Ll*@$oTgX<+2)vsFzDbamx+l#j;B;SXjT=Nai4`KK_Na@b zpXB^eYS|rW6?oiX&{eH%&{x=%9E0}}T?a#iUx-T=jPK^N5dcgs^%RaE9c8k5#avd; zvWbp9Igzq;yF9aVZNgu-vcDg8YLfKT(PqpaO)_hc4?b!c8qu%-AJ%odM0_OQPi0Uy zI<86HU#L!4YJD_R!lwYE!oDMMoJC>@dytRo?cfqMJ}#rVCY z!a$e2tIz9(3)!k~Dh+JOCtk6=Jdx|XFq3$3wXiW%s<-Wi4!U(Zfkr6cs9p8fy4*L))o%fElSb)J6OYU^cJC? zfdI6t?Uu&99r*OFs~M)tI`JFPC!$}vS3jAV(tT2vX}98ekA3{_{Z}QU!x8l@|5KUo8R}P3kZlNHTqChwXJmL)U*ax#=$*k&Lcr1e%@IE1R zz+3n+;gWIfl2*b&>m_Xs3N}8Xo6%_5bL!o9+%;9uHGX@vv1l}Wyq|TPc~j_oY&#v|eo0nkw&w z`2wxbvW3v>Vbn;VB6T@MP%;d`abT$=L+F#6a{6S_rCn8 zUOzNkts7g^^j@XroYV5|7)yKH)bPkuW$o&I(O~2&AJ0-F9QY0nc60KpIO!|q2{_ab z;@SlGPPuX+(zts!iRa!xVd5b8aP!Oi_5H#PMezwM-Kis^5yN(_McY-)#xEtK(3_Kx zdq>cRj@vd*wry9p@A_`vBtp%ot8Y+L^*p8vAC!SU@mVi7@Fe#XjW ztdb*Yl|#Bcw!Z&w{f-LE6~Ar$WcwioVx7L5@O;;LeAg}=45Zj7{4q)OWfR_5w&Al= zt&;8hrJ#N63tjq>Y3dpqXUPrwZ+7Ra7r$0U7{7)auN`J2Ebs66aRMzSchgsQ;~T%X zukPOV+xEonDX4Dsi%lto&zhbu#lb1wXMFhQ*vOH z3h7$?{#Etx{VF6Bvylo`_=Uv2UYy_o5}$K5VhQ)Oelo$>6syJ7QiR z>b{VYV^v0uS-&txjsS~Shq~jUeYasRj$|BmdHpt9NylpGWe?89U9^p~*iU9o#P`GF zJg*?qNIrBg078)BL#?Buq2wYTG1-&Ih?5{giJPtVHh~`>ihf-00q7B=Jeod3ny}Hq ze}wx04yY5}=06q^_D9R=Y(x+r&d^;HU1V{BeeuV6#+tJ0$2G0sY565b(}8?sZF8Ba^Y6D0q$gcR(3vUQ$U(1dgJ2>Eu!};X-D#+ z7wmz7v6*_*J-ZU_;F&y@9z~-iP;e@YuL=&D){-RVhedw{DF)``YK#0kfR1K@JbAH? zT7ZvFMQ7gv<<-fAaC%*ty#U(#<0&j+iK7?XYW?htbO#X&sPcZO46WRQ&|m0i6uAmo z6V;2;Nq)v>@IWDS*KmWdPi(N%X@F+k=-;| zK9p;x6;J#ML6F}H#hCV#MPa$3B8eWT#KAlTb&4kBbq{z9l8+&gl>aMQEjF4>UoI5b z2{Gg*w;jDe>BPn|R`c=VG1Y^qRDmAk%FgTEOo<-ru$yjvJRmZqdgE&j0QXA#ZPQ4~Kx1+A z>q@a;_1cfm$C0IiY(5sk+FY)-(Qyh^ck6U-{G1*cfOkvP=?M((gJQX#t&Hf3ylOpe zY)h6Yg^`mLazhHBNqQhCzNKDjPm12@Ed-53Ll|uVBxfl1NY*-zk-GRL>Z>Yizl74w zqkt9#ifX5HhJ#d8>6tp-k$k#8h$mi+Ie&9hK{NC|H71r>)9fH3Q7w@s_j({KPBFk3 z{*U!HRjtBYOq4vMBdRR` zUy6^nF1C*M74h*jIh@fxKULtRg``2!Hwt;DWJn`3-v;j z;86@GYGgy}F~Bay-0L}fwOqF5s2xOesjCJLsl9QVQS}@cKQH5`MnLe%*Q2z2Jfaee zpN6V|$RiS80WrKsB9>RSL&R>TSUfSFgILl5;ijq2DRp8)(nINEF|S>kFQV0Aes*d! zCUAS%Q5%!9BCmhy<|^e*BI&T(zjoj*No&XCFLXHp`ZsHsNmOQ*x``vUU|re z%SW~LkOcR=2&-%k?lg5e9tHvcoS~GQa?a^dZHCPQ(K!Mox;XOH z>6529DvtA<)_B!uhQe#~nv;0Gd-;?nk$W#CSCDXd$kX2am$~=*6Y!CkV%gtcHVcmA znW~t`n^{}mxG3A_-*e?3^6=hQqanT1Vb5GJv02Z$O;!kiem+R%rD$D)1y}Si=kM_8 zcsNolF-3hk_z7cP!I3$wXF&NUC4PP)oEi)EK_Al~*hJTL+|Gea+Nh|lENTGFLM+sN zRCmbRKu?CbtFn6h5w^#y`}r>Y6G>Wui8^b#qP&3^l|8Bt8ogZ5BN)Fjf(%l3k5O7q zF}h|djlcBXfCtmyo5V>z|ME??KkbZqEC)QRn`KJU#+Jz%->K+&#CvMKKf z&r@?P?P*?JThT4roRhd!g}o45el$}bU4=a7v=jZOav`IdkH1`}b8d(@5xXb#<-4bI z>sl**8!w9A%XJ~oX-%T%Baz2S?_!fP%Xu-b<~XCyPl9 zo;!u2B^{cu0dcw%P@qlHtVik>$k0~uzLXz*&U?GSRH%yWrO=GA++Iizn0Qni%c$;4 zn^%)v!N%tZf)eh79UO^JtOYLLUx%!VMbjZbgN%aQ6U^!e*l=vhn1q+#;wL_*4DACj z!F#F1@oc4rexw@4nV*!PX?V4jbmA*&CnWhw9ZD5n1jXRIe9!5?hNjGI16XR`^Iqsq zZXpA_40UIZW{iv$s5I&*6IhMA4*@=BK=hLFKLS#&VBnt_0JqVJ@Vocq3nfI!sPcu> zC}P(^pUYF;wL=b#LaRT=s9%nw0pTR4FZIwm>1rzrht`bh>JB6nQvg zt79hzMGsW<1UxVURqt5js?nAL4<~Bs3be3-iIWXr3tcR*78J*}!Un29l?Usp!5tlA zNSp_*U|2qo)*1;?lc@t@a8V$0!M6QQN9qb&)7e^LG*<+o2kk+q;x)ywiV6>*@B35Q zBkwC+*Ns{3;%T5$(MW`+S{kh;O@t#};$$BzeV(%AW{O9`T`Wta=?fkq%IclcfS-T? zscprM4Xg<0mI87Oo2^q=IL`+`-hMo5!1MfY=DjL}U(Dy*s5ZT)fs9zKjp_w7wqPpM zN0}2h(Bb(dXWmKdlCm-u8N-swOIE~UN3n7gr4&gZ4;D6TiNzJeC+9429TTHFfEcc$ z$Ixldai`ly?h*wA@`t%Z+Dis{doF?rb3qFvvZ)Rzw&jN#W>}QV0mhS7_J+P}uN6|l z*r=Y6QE}W4p*$puXB_(E?fss<)%^%K({6}DCxWAi7}${1`dHJi9tTZ8Vf3pvo)PGAb73=7d#xqb1<}PxmCc92o3com{2jWIuuZSZCMuo=H3Z*BQ?D;W)#>Z;`2GX;I(Wd^ZV&xpD+E|+K}OE^jAP_0YQRYn^N4N&!~;Nm7ua^ z#?QDQG&HBNEp=UpcK2&l9V=Cz;ZKd0D?sU2DKxZbdFL+6t0ANHyZkcM7r)_x-1|6V zs4x#<_VP!Cyl8N6*X`$bOx7#eg_@4OX}?58T$fB?1Uhg25W z0haWUa~@sieEVgQLkfbeoaj`67#!30k>a^wMLEL@jzg!~=gbiX%OCW7WkxDMG8d|w zsy^Ov2w~R)r0bh$JKSP7it5+Uu643BG?->b-0zW<23Ujj3mmT3-*37F7{bu*vE|k6 zIWoEh_lxe1Rec+i-y^+?9IbpQ?GyFrXTO$lm7)_=$0}y-p$0viOm=|5y&TFh&1-v7^;ZI{Nx`ws4yh-*J7qee$YFN z`_cLrj3A9txyvo_k zMgq0=7kWHV!~!|Y^0d$((K7@SR_MVkFzl-e%awxu(emQGlKsHjB45jVIc{E+g^#;t z=OFXuTfs~~)(>SJ>me-A!$KMAL@AaFT2{eqccTCeY8FYAs6cI=Y8C_mUOHgjf7fQ4 zMDn_lt>%eBsMHB8?!k9pVjF9ppkVQ3mQ@30ufnH*Q6{tJdXB!2%=$a8LFzA9OlIaW z$vftI=k3RKof7G5dxX3O{M;HSUWgpbU19i#%0 zt=hcABAcM+KO@0P=70_OnLR0W3ZZzPljY!Bu6_k@pHIWJ=`=s}6Z^3Ig$Lw>WP&Tm7fg-emTy4Fp zQ;~*8ztFu75Xffq5fQl1#xG_`#P1YS#9C!Ag}(%*ou*B-2Kr2b#uHT=qRPSQ309MG zljo(xzqMO6Mj5kgGAZufx?yJhLF!E9zFiPVWV}s+HL(4Gd42X8b)H#pUan?K2bcHc-t*UdDs*L-ub6}^S{o%V-I%BUNzA7EJ zvVxAZa2Yu1Ojmp|9fzzamcNZ*&hCkZ{ zCr`R{>JisG*x9M2XptLcz(hu*AKNV+LOu8osKCr|w~7DqA2TP!yWOyny*lHCC$9Zw zsb241i`cWpFa)ipQg%-9=!9`K(L{)dVJ=Z%*FBd z+4$>1b^u{GNd`-5tN=naQ6g9%-3d_f6*!`W%J#g`jwA`s5qmk2a$G>>xpsIYX-ItS zsao})#cG!uPgniSWbNn19@0C-(dgd%%7~_rMZsRf2tz#|FFC^mKt9I8|fek~-MvoZD7 z8#^%}EM$=es^@Ki(tJP{$C_)0>ZVVvKMK{p>36N0>GD>~d_B_j)?NYIA?`=L!?AF9 zqfA7Ty3$o<$>m@!i+PwgYEGK9BXRfOdCwRPJfufOsiEtlL1-}+w|H8$yf4}urWw>~ao%o9d~vJ-^G z&Y@tML}4^b*5mx!^u7QrCiT&jji^*<8qF@Nd3{0c<7{y~l6!~ML@|HSQxi$BDee?M zq>QVs*>2%66I%;@&golTGBzL6~l?b;=bsQ{IfF?O+k@}kkLqm^kDp)v5HeaH>|?Vt z5gzi#In*?hhyHJIuM*~d*T4GS@6dUrkULzw=4*kem?vNFU;N+2Ai;te!C92I&wpQ}@7@ZCNqDUnK(lq-m-pLt7H0b!n&Kb9P zD}Q$?KkT`XfB%G`@xlHFiHHJ)%^H_ITh9pDzTJ85(3ZG!j}A)Pj3YAWJJUrVg+M^& z9Fl3}yaI*Tv<{M{41eI>Y}fsmy5yzIt(_n4i?j*tnfVM~rcE4#-#+#~U$pC-?0@z8 zR*U-Qm76;(UwoM)59DSlm>bi)KNV><@|OvuDP@yH(CVh(h-b_Z!k0E$>%GETc3v-y zEK95}hSM+FrtGs6g!aVU{pC_mUFsXoU*D7Rx2MDJk zF9tDds!BHkg?Lk8v{b(0piQOC7u}C~5-n-c{3S7Q?8)(={cLGavhJt*(;e@uIB~rg^bpPgvY&apvLO*_{-$M3Jtp@cymh@n!g=8FJ z`$)0-mksMXtM@r&#Etd~;pUEL?(EW9k<{g|6f!X-I-kfy%w~>k+Z#Gt!9|B7^g}61 z?g{Ad3`i0{e@>=**hK>Ro59S2miTdB_#_4P1Tq;I2{w>351Ih|$%KP_?h#qd9x-!`U7RU!`lXdUe=G&nw*anp!fH3|=yoq4Q%a=8BVmSO~5=k#cA zxn}U)x!-)}*4}T^XXtwLKc_#xfBEunDCl55%RTHnvsA@3fJ`qsjamByo&NIT{3@yA zPMc>&hd8fD*14n;k!+D1NkZl$E?4hRft+5^T>1W!^)yWNStXs+eYjXYU2Csbtl-5n z*Y)dCgK<6u#{7wxLIrx6$PELQ*Y1V(7Fy!P|EzavL#(~ceTzMleuWa^AMV&APrNLDLJ@V|5I^jAPu{KAELaxy4Ms`~m?ym(0jH-AHQ zHdEDEEB|4nGy@+)W2Wy~y2#)|+ zBCAQ-2nS@Vl}3;A^26I0SfvI~k2qDR04nP05fK1#up_6^IdvjMNW<(DYcpnO#fRiTQ27%a34WDI z0I~6#kyB9^4qeHbX766{@rW!wJY4q;b^nniTvBLp(4mj zQiePW*i5y`D(2|y(R2OS44qdWnA3Pig-`9WjZ{CdWvv*Bf!eN{{{e8;NR3N7wB-o9 z64^R-jN~QSay|ZdBYjX1mN-_k_lOH{)0H%~7BoyUMvU>p%7n&(Qhsb@&AIQ{Pj#o- z3gVAygn-YU>a#18C3Otgav~v|r*yWPd1pP$J`@uc&dcN^om3&-i<4S=RfTCDcyY1Y z2G|{Zp}5CKY4b^vTGBUoZm1Z>ly^#B;v4*dlekPokf~kyLV+<>oU!KzbO9O7GA`%O zUU-<{!`^XwV!BLWnfHacO-K2{fP`9G(2LNh=!$o*Bs3rM&9D`c^Qzt7eqAUPWNWuC zmW+|qc`rSiun>LsxLH!~OVe!fx9EF#++~y*Z#o160E?L6aNzz_G%|((_;#4X7d=Oe zLc_HuakyL@Ak(F@8X-TTZ=X#%SA?xS2W@2JD+jVtVye$msRoWRc9;nOm@hj}{9mF2 zF$oQpodn{=R~@fU5a|FVV`sSIeadQ9yuB-WBAk2-zfXq_` zo37wN*#dwME+iJ)*5>f|xm5JY(PLY9DuWXN4BEjl*iv!KaI(9=D+>I9HI*eWcaCdL z1WZ?(1e`~t$=6ma)vO0j1W@g zrmH|YjYNPO8ESwU@fgL7@lMcE1KQ_v-3QukL?b6KODmN(ltVw)kJ?PKYqcBewcorY z2-&na8DNi-f`N!{0Xip)&mPJoG7`X34n0f;jRz2voS%T82#a8E6btAu9kN51-gs!h zr9%L-BoG+h{f%Ib!2xuAY0sZMB$@FHq=^z}kDZRg!?xx8`J zcu&img^he-GAn&C9arBT=qU^4qtYB~(E!2L;YSR*4lmeyhis|e9zBSByQo*y&Jmf$ zz)oS1wp9lN(bb&q9j2-)YQo1no0wfFS?4x>bt={=!&2vnslSWBGIPMp+&QA?SQS%} z<}Yi;$Suae+wBe#SP(nz8Q0E5SY_Q09GLda_MSSp#1qeDmmMKqw$yD)__1*L4p1CT z7*>~3Yf+-U=5M4ij(L(YpY~6Ke5-&0foCh^+&XtB1t@hucUHQ{?Q-s8!qd1%=ient zS#y>=PQckTkYB^IoY?6l8MET+SDH>n72h4^+v#~bqDFj`*JCOdlY zm)~Efi2hRuH4apk5nXnnb>R|M4}5z(Fm68AF4dr@8+s`WKn_Vu{XzumdE%KnWmjLa zqgi6ntwKAL^gsPSJ5duo%Mbd&(vQ$9(I_HgLK&nN0C00>z*1pnt~LrDP`IM}0?(^S zQR)(a4pu;2XpGEv&p*Vye_^(hov$tOl>EfegZ{Ueac_~a1kKb!Vd|!M*3l54rIFiC z=EX%a8Ltp6IA@Xf@Oc73nv((G{A(D6g1H!Pa!RpXM%X(Uhyy6E2?Q=Soc*iw_MGf| zA3ohKGQIHz^_K?U#4%&JN=^oNXH?N`2FOdWvZKKLVu6y03zykoUX*i7B0igi zI?PL2civpCdjmIT*J2hG;jR`D5*3xG7F7@xQ}-5gDHLN_Kc7exB6E01d$Xa}#itfM z#H(4*)o7D-5wCR#Wp9Zob_w=%Y3^bE_IXe(Rkofer$FVZqjJr!gEqYDsG^E9-b_?> z1}YxQQiJ6dQxd9Ck`Pl?s8LoIqpRrEsF;YUD%42CQTTSv$>vYlRq-5?0F4we&CD9j zd@-$(8m+rx+VwTsEn??$YS>hV&v)18bbnMY^wF6S)4Tgocj}{F>PLpLkNWC(Tnbeq zg{Z;3fsgXhIrh<0uQk-$(BUVkDPI@%7yIjhF)=V@xIOva*$kdTfTY)&N~wU&bztBG2x8I1K(t@S(18BEFM0Ari8VN_^t%92e-J!LZdHhE9Y;KLJf@cM~3DG=4`-fH&{c7@VpJ@a$jIky}1e0 zuf@-GxJtl#u4bhpk2CnsxBY@){T-P5Wq!%heSv^F1Xqmh;e6-a*UY4Go%B+aB3bmjv;-qNW#%oDZo*oA;jXT ziT^VO?`Nj`Pt6LO%1ky_u|$DvD!UmKK^$f%qvn|q1&pZ3S^&}vfS4u<98wWfBI^lY zS!%CJhT4q+vSIc|fYg@NguMDrDsP8knmD5Gh*Beu3wfA_9tV7h`$XUXU2GflOsom98ezap>6> zJEzbvcN#j!{&|kRKi?m8+vMqO5Kwz_w!D4Ue0|f)MSjss{*wR0#l6jL*BvC5ikJrW zbR((<0lgN))!t!*=BI>k>xr{?iy3i+NXIi~cnAt#) z2C2l?r_`6dEh}F6?d?Rk6KU`SN0pJ~P5;awg+rE61C!;43}il@hP@X$K;ooEO7b9t za5HY3_o=*F6M&)24fHg%?-bW_y4#K2MTN@NjSxYHL^KgU&kU$E^Fi`PpjOFZd~&S)d5aw$BM76NF&xB zfi$hb#NoDA!Cbk_2n-PtN<|$JpB>?Xgu;*R9)4gM+04dWXA17=-a@Bu{9(bvD_d9x zs3+fW@IxYNAZ`H(Wb4I)E89V6z`??osx2$m#IkT98z`=#y${Dt*yZytr263!0 zH~bXZjAQPjH8-_Go@*`V2#ir3~X5OI|LJvdtDv> z-C^shTbuqZ2xhtQov0Cw7t*z6M5|Oi`-TM>LLp^J#zk9rBWg#B3<5=hyFRZw0r+`G zFkz2a3Hj9{MEF^ATyqGrInrIv5!j3CFL8$UQpqWJZVaBgxeKyF$1|L46=m+uvdhmJ zeD^f%@Y+_pBoJoKE_s(%@|scDmD)xXpU+DY>fVaoONG1awN#^s=T3w5j;EL)IlSg9 z6&Ax3XQOUD17ssiUozYjR2}YiigUI&cJ$g5#Ku5kn1VahgU)Y;xYUJ6ZaTUg1~{sd z)QP^gFrgKgpg(FBEI%WJe+JxB4@QuD=*OX@6Csvy!1OaSzn|MPkqb$6F7z!ot?yoM z)Do6s6ToK-sGdmuLoLR*cyvSLj{0(Vw9S3-I0dXl3Mr^!D?zp{n*Z41TaWpvhASu` zaHf{{67!$%iD$#2o5C%R`3@BtSz2ZP{W1$qEVk4t zPunSekf>ShTi)=iDAzYfG@)|2uV`5-`(0nfQ!O_p-@E^Q-V->!$j83kF#wylj~}_ zg5NsG#;jYyu^q>b`Pru*?;4SN5p&R`Y#A(kKIlJOJ4`1)nFz-Ox+|X_HKd%w;<)7= z3$ol&{i62J^tby=d`rY{tNz`n_N1}i)2ppLk!xxXBXlya#wXs2pL&dWRFORH(+ffm zz}K#x?Hi!}dhttY9fY0WsCvhE`JNeET<~+RKm-z{8(>0xB1tkqAHkBx5h&oB-blzK z5R8E&R}UlBu67$y|5H~3twzQv<5&ZKY)BDVed&J@tpIR>QM2-M7<|NPC9ID~LMOo9 zh7;>Ce;v6Uxr=ZSOjMqodZk~)b52A^Yb;~TE;g2Uu^6B{q|y5Tvp9nBk&W|-7~HxL zmY9}2*BBp9)s7tNnd%wH>=;NK(&?Gi_Nd3fQ9y8LKHm37(?ngz84z+Z_-4NH@gVMO zAGLH3INd%In7`=fF zyqtvxpBG_CktTwOMcL7VMsvONn>7=8=gy}p65psCW^Ff`CFH{ zm;mPcZUxxh)0xfI{C+FY9xc|r;?(aXF?Ea0NKY-uH+Yfk^vEstVqu+Ai`NsAG7kKt zATKfnZeE3-%;FQB2(J7NvI2G)xKepl8lb6gc}(EOut7RpLwKu~4YW1t4O2X$GS5+; zqIsEl;iL~HBb7qz=|(4C*NyYqqWPxT*2OaR_;d-OC&093#x{00)a~4k>E!qZ_AnqLkIz!xxOvvQUII~A-sV9(tq6R3@OnA~^6;#8Yj7#O=53~ls zn6BdCRKm%b2PYeaQGlv9u4gbOPwp$dNQQp{i=rRQtHOBEpECH^d5u6TRKRyIBVo>p z#GQf-fpuq^Rpo;+-=r}}XY(4SDW%(?p%7PEDg&n*Z#BbSf)o|j$8w(vVZ)q>Y>XCx zg!B5CRzsqtXRj23Sz)Tc;E%~4tkEgmU2W+?z(eOH_n>j-I=GlzD;~n-d$?RJw81sP zdP@Hdl4YqXKa>p!$~e{!(yi{k$Q*sGOTTzzDv*Uc34Q^>C6@gmjWflL4d&}e5XlhA zCJ-~=OgkbVHYV3pEOyM{+uBt=&7WzkNhcKI4To$3P24-7iZ<(32Fy%%Y&-!=iZzx! zg|DmiUQbn<318!7awVSi0Qh}amvBu00%2AU+Ojy?KlW@2CzX|EfV@|R)7z~4C`1s% z%z((uVzHi!S$RWA$86+EIlJ7s&+o$4_15LnLvxUwoDcLUcBML%J#PPAA;=x)dwY@9 zz2!sxo6*pY7J=Gdnj^pKo@}m8Ds+#p1`i6mxh{)8`C{XtFy?*cji8E^9d`E8w@z?T zj43N-gN+)tUC}cQ`EL$-9&o)qdgxqi`MGXP?@a^2^Iy}-VsTVZcmriz@#li!>R&hi zFv)XcZ)~?&uY4@k96Wgp*o&^ifD- z!5H_>o_tNv_ndIRq>6uSVzuLjuyEp}{@#^b!6sj{WRtN~__w_4W0>~pK+3;gk8*&a z_r8>rS~$0~i{0?OF8e{9+qFGPeCpF|GOqNwVnTkYeNEk3e`f!BMtja*cRMBP*MnPZ z5eY9|l{{>RS_f&rbYA<&&+iZD8+Zm)PO2DXqyk3r;Xs9ino4zRDroFAzCw;ngindi zlx3sOi(O|d^kxT2UpF>xU?XWuZ{Ox2+M&F^lF*!oUA(ZP5Xm#(lB0JNrps{;%+v9=W>c_K*jo#UqU;K?}hw zDhc)o2S)%K27jhX20-{}nbTExRcKi*tM^zj=#U1*y1o=lK?`L41>CiION4pZ0m`qL zi!lRU44;U?In1aE!FWT&tA)C$;f$uh6#x=842t|qfW>?RhC5Kfup$aW^u>^1UM!fe zi^7nGGEf)8rgN*F+3^DbNCRr>m^uM09Ek@fkyr3PC_st8LcfdXj42dQ=G!i{{;U%l z)Jr+<_>c&|971_ks=%MyMt%2b^Zoem_4D8+3wlqMN1=AK%UDd4T@GWqY`0<^UF4>9 zNPkt>?M%Nr5!xTiVPBNR3S(046` z(4|{S<8E9k@+&wd1L|VvF(*=fW!PQ$Sf(BAR-e75itXR73}~>0u79?L=Ris z8HTaUmGfkuP(;=7AdjLJSe`L>EC>L~L_>w709wqvBgo7-05ehq!b};>55dBO;d>M1 zB;Xv@JC(^74`4>&7~#M!78Ix_4ogCK;NZVjfPllBtp8C9bwjv+_AXjqU+&&ovlr?p zdRcd1s$Z^MxgOkbZBW6cgZ`k92L(ii1>wCW4uNbZ0Hr)XBFqhy#gh$CZn|z|3S$Fm zv;vUW9TIG<6~gSxVG|8kbP&J+WQK7Nau{wxm7FT^S^|V;;<1zdn=1S&!6s7NBUd}E zX$F10t+30a!68ur#u&QO|@OMAv?uN;5lepZScZ zwFqK!IsH6>kpwV8-ODx`79I3ZxqCCj;X>bw@aQ++Bj1GDo|s=r4v-R7{yd=P)G}31 zt@X0}Q~7{-fWx%(+cYxeaM{wnw?nTnHe`zmfWn6%x^U)8*<=ATo?|)o$wG3Wbwic} z&g+i37uW5E00xG|5JjB)9{+@5O(&t}~9g`#Y zgT%zlVE!#}K^W*IURPFa0pP7LIiF*TVXBz{_UiRI=xr0LUUq3S@|+yV+Ke>De+X7) z$@BE6EH#;~!}9;29rX<9e!uzSL;CIh)|Go{u5Gh_AdWFX`xjS#DUHlyq~!k&?CUJ* z)V`$ayF1RV1ut%08h5a_xI5$AJaEb{{UUw)J*{9CY1g-E@|`~%XfFz#GAcM0>%4nL zSj*EFY{jT=N9#0HD*Q245)~dj!fRJL}$Um zS|fr{46Kq-n1G7TApLMGgXdh9Y8E_e4tlc{dS$LM@369Pt|F&3!f2A=#vDUIE7Mge zhPqb9AiE^WQSgL%c6O2*PTe#e+yv! z)yn$!kQJzef?h!}UWwhGi#19k z^_4^8E8Dp%%mQr;#$P$j*|=h{T;^9etZ#CeDly#=SMawNlc?nmW8+j)V(_%*y2i$B z{FTe+D^C_1?~OKIF-7Q$?PNM@GVdW@l@fp5&62@Z-HFKPsm~z;o1*n?0#D`>Xbp8+ zp?p38Y(W7Z29?e&w&@Xe#b8^6%-NVh0#J1#&VTp#ZbNAtqga{0aC{IrBlpE&#JRHo zMqwL`B^>%O;C64r*T$e)llvkX5K+XT$mdy6!`39B7VO~`f}c1jbwgb$!%N7bOtqRJ zfeKB=Rjx-y82}XAu&un*Pche{x6p`%Q5G2-IQCg@zcH}JTvVyBzFAU~PI@Y$ZwC>{ z4`%ts;!OUw{?yGR5^aitTwwg#FS#DRjWDwT(w;$Jz>7?@MGqUG#KWH{6rewq*_&o$ zA?xG-U{?N!-lu$0$0?saf1ZqMlo7k6j%zow0juJGnE+-30$df>*b)X#ATpfAs80>R zuY%zT6o?%TrBm0nvvAN(L}6u~pNvcyWdQeWf)fbH2{aPP`u={djIpFNM`WMExA&dw z;^pjYzrXxfelyNr`h814%(|_ZF z;AFJ+YBVy5!bJFnZl^*A6%qV|?$i*L5h8G(AR_Y;avaWL1pxG-W0aLOALXi@Q>K|k z8fJX|yJPpCT`TRbGHZNvZ0CdbMGgSnS2ACizrR9isuaGUDMH9~_^OhrUr2}F>WcUMG z0A%QA(H@+xgRKbLGUGS|nlYOSDa1`}Q9!T|5zPmX{*P!0Zv!>e$8j=WEv7U>I?D;K z?C?&}BW*@suo_%S#=|D|en%RNWT|uX2=z-&05Kc!S0l##(G(+sDP~-4gSa4Svf+<2 z+pJ@9iDgN~K_)>;{SO$7i12=!9X&LR$p>bk08&3|h`P@tmj`vjp}OUljEKrUw8d{s zFcc2lJu|}n`7B04sIEVt8X7!72Dei{GDO=&u&P)! z|K&@#O>3D;x1hLlSoR#TwKeo+_Vt^bhRYjDP8m#}AXOz+jhc1yxacfu2Q!ntI^ z7fZq`TEl+Eh208_3QT1&<*Z&^3j1&zdM7aaUgQ=1oru5P;UYaz(ig(FyCWVS_f5{n zPPd`(8ZpN4G3Grn)<0tyj0Ar@ik{tycJ4%9)`;_qj|=LF6HxHW8mGDb3>_)CFgG7Z z`pM^mWeSsK`j3o}I7_xj`e;SgRBDu_Y9wU^MVsy#i8-fayCijKgeM<+jja#H75J#5 zVU?K6b|WoYd(;VUdSEA`rz<+zBNaO04Nd|GsW}R7Q@{|y6uDcn=jVStxk(pzJ{)#^ z$VLPNbc=?lNb&JH-ZTHYBjMt~Dnk}$Y)t>r9|AsDfD#lp2)HdFtPkz0m@ft7iFS=2O~<8LRea@AVwJ`4t3zu z`FAX?UMfyfjxqt~$~1*t{q>J>x>j{5_@ty7Ut@{Z!oVh$@ z@KTg!q!8{QVPg&=cwmX2LNTlU3mN}JwLXjHKgemvA>X_MnS|o*d~<}uG7ys@8-srX+zuDJ{hrLL>Ba#YxKl4?SU=m9gA5ywiCSFX(tgOaXHc^g zSpyBlC??s;p$yG3eaF{=4ro(4vl~^sDhDVU!vqbQBsQN+Q+?Wo- z0brR_H%CJ~2tjAi_ADcfHll!pUa`o$RM{Mb0?=AI_YJSzcKEG!skmL{*J6+(DE`0A z5Z#`5v5k1=CVvht+Cl<}LCv?pK`ey^o_?tk9sw830!p&D3!)wmznaA%k>hv{kid|&KuYF(okO3VPUP?U=8&z7EDR{!-s4`|SNj4E)m8CoHP^(oz49(-!&SI!9^3wU>`c;v#<) zW!exShhH3gINCn-SN}}1Z;CtzwoB6S7LdjS8xPpM4Xxu?^SRnL8WGnPecS%MX>9RL zqW#Z_D>_eOlb_r7} zCc}32^T4eB?yO?d?Bl?^shuMm+nALz1eNss=2}7f!2Acg$Mfe>(_(+;b*sq>*$dKd zUzAqHLC(i#Bqont#hE3Ydm%u)(pPh*;qoW<#hc3&&mKvfzb~Ow1hhRzykJ3Wkb3nn zS%hogYNNn}IX9tBv(F9uq;^U<(?9t7Y%)pKF7&EEW_{WzJjh+PgX4t9MU)*~%XK#N^0Vj(^gP;4hxa z_;VrK<5_O|O4i?ZJZl;~nzD7gqo1E(^5VdpvgsVnCOdCTh{ZrVlWpJPg`G_X3pB`; za#tOPN}&K`ygEsXFISZ>?}-mPP$1LKS8+aIvabi-$Y+59+WfM4b_Vcvl@AEV*q7Wo z8}Zm?UG0Ms73!><3URy7xoRl>EiZfDTeFAo34P*ly&>l1*~c)#3K@7>PZJvpHsqW* zcko`T9B6njSMLOZ0Tpzj0_OfM%zqJnNxh*^;XX7f>na4qs>e@6%ZAyq1lOn za%5~fY-+XFh7m>&IXZ0gpS($Od`2QdvnlNM9V_o` zwIi*F9qrc_wyMp-PfyC2T|Qj5VyDvY@P@Ix+5qU$>VJy^w{VcViE;{l<_?DDfHXxG z+FYYWMGyqs^4>m^#sCmaM?u)q19=@$!(=Ufg4ykt)G;zMbCyw{o%c+oVl>2eO{&T$ zgHI5eE`8ZFHI-c+5?bj9!ey6U5?yHyQfZi})V#8{pBNngp%}!U{(081Gf%O&$?vz< z8R*#L6c#*iMjcstc;U11$>I>f6Blnqym^>5|WM008}dC;mdkc89VY_ zgkNf!m)!@BgcLg(CEQN$NoR^n-k(v<0{-R}_kc&Xk;F`y4}b8mRJsx!7-8tk=6omb z2~Y_WbSi>>g_4Ha!d%F3PaJp2^k=(SlF8ricRAbj8kI(ru=#Nzor$S_{W^z4BO%ZH zFRnUK*NB{&$d(>SM2VsT01I(Mh+E_k2!9|Be%n-0AA@(^a$B_rfnFZ zn@s+Q5HO1LVL}3~i?Jz%0wx&blBrv~_g{P^1+%qAN|q^R&y7~d)lv;uMb^^27%wJ^ z;D8sMTIcCJ`4S(bz<-~-BBcmC%ii@Y_s=OMRwngJ=P@veS7*B*~=Wg#OYO94TcFb=%HFVAh349iO zVjn1N&41;{q4%BJ)O@3?b{&s%@7>;(huP-v2}d4%OD{PQI?$p<{1c%bvG8sZB?@Br(l)2}TI99G|&w}EmzgMSOV z_NM%v2_q3Lxd$Wn8{^yui*0aN^ zubeEy=&?)u&hl!@jv;^5mOOLM9lx@=_vFWlpZjOE70H@wv1^gDce>YNUxvl6MZA9z z|N8meo>1Bagv|*p^W%+#x1qA{G~R_jV2*p^Ys&I#qvX-spC27R>8LK4h{=~Ms4L>D zo7u`zw7ax#d-d-%bqw}@Qh#Rgc(3(&{^{qAHwstx9uLv4ejY@8xq3Lnb1C`LsK8M2 z_YwVBox>RwTb-}UQ3JQW&7BPye!ht)(An>g7Ebx{UijYQUwgOi>1Al$V@jG292Z_1 z(t02e)hM(*``1LG#KiE^5($2BhW;f`9^Z*uj8<&+-^Cb3_o8pcNTRMZ2ni6;uI85#DsT7a^7J4}n!a zkeE19oFO4mCC)|0F>DdCU`{lM_zDqWLBR2-Qh*B57X(Qe`Z}QJp4B6FM^@m)bR7eZ ziq%RK@9uIta+NhedT(7#cd3OuADntG#}<*!1>)|dG5EG4S=HxKxld>eiey6=gbfVE z5@66tYlHzBbh<)>rXCK%lCW9aL=lh*l?{|m0YJ`v-a5R7FsCLG?2rN!*)%|)XlY8* z1SDn?7*|ULaL{(eq1a&<1aOBLHNqm^mS$^-N@XvjvM2@t0ApBbh3>Cuzg|`kNHjV< z*l3P68b$w|2vUjGoom{zmEaceJrg9aG;ej9gAK?ak?G5}(Y$`FugahCOv4WXeJMG5{SN-W@awmBvj+UPdaBsi@3DAhZ+5be&6D zR=&c8aSl`ZzTejRgCmKZGR7i8D6mv7geXmq>CcDyIGxo=t-%8CL>bw^U&rd+-$AC( zD(`8=`(w|;cyjw6Zk^kPdju`jRdoB=8DB!fxhJyVwHx-_5^7c~`yrK$RmQ#sl9E}bE3!EH+TbG>IZ#eXP3>{+XCGDR%1 z4+l!`WfifQ1E@}mAVg8bb4COP9+arQ;uX9@>1x}~Qd^JoyGsv)bF0_BabG<{^gw^b zuKm{Ka{OW-;_6;0%?-WnC?<>Bx00OWKBD7H1*KjbCzeG}0@YKB#cH1I&OZeMF57jGIV!!LW zJ@=2hM@iz-!&4ofCgf`L#1omQM^Ngps_gPWcS#2^P>!Hswbbih)>?3mO0c!;ec3P4 zUhT3UDt|S4!QlAOBj$a9;NQz{6qr2QU&x2Vzgb=mFYCzg?OF2Tq0 z^=N+llN(P$n`{V@K zzvlTQf8-S18q=Dlh%uf@aT$5f}7|s ziO+QD(WSpVcJRXe^SQd8?k}y*xxSD&|9|SpUM2r>V!v^){iQp59$!vh-8yJDNNd?u z{YY7!&5wg0Y(7t$zv)EY*cANA8>?+T%wuI591&Ivh*~-iRxCcIBD@HMOBXf&S;v)E z9dz5&g>$#2e9v(LM2YWlX=WTD&4FNYPniV{hmq8|RNEZN zL!@T2@iiZmA=y3<=fLq5T-pT`9k)-#!VKp?3KSqe6(EHM9>m8A*bAqVEp3e8PymNW zB)cdco=%3`s0R38VSM%qk?A(HW#iR`;J+M!QvS+F9+UZP<(FB?F;AVgRTGDPrhz(w zbv2Wi@(Is@j8MWVSi!Gs014U!Ehj!}04z6#pN$P1zwE-H2G>-E7*N4yz*)mN zz*+a5&zv1yhek}pUKFJOWV-~ID8O({ls=iiR)YbGgGZAg7v}&cuRsz049Gc992IP! zmLwgF|FF338{9%9Gg=)bQp3@IdpKWA>~*Tm84ol36+GT+I{bSJnnzs z`tiCR*Zc5%{SfH`llyPoJ%|rWTMW-Gs`u0XKu7aj=GI1*)O#x~f?ldPb zCHj;EK3om|@`<=TB6h77WyFv07u!X-ewusnRQ^RG&pGPM#*vH8rKSh6`|q4E??{YD zyhv!z9&#>KH7zSoE-QR|arBo{*Yo7rsj{K~;P_)N8w)hv7jSYS*jE~vl6U3DlTJGs za+0D^SsJA;BkvzOdoRUETm;vQJoaP1mopRh5+br~n6bm1v7-%_$A>SAQb$CX=j3o~ zdDxXp30J0E%{&qmg7#fG61Ka7X}Omb`yuM;Y;9};B=$>#`Nym)-<8c?ez{U=f9+@4 z6-vUjFJ;%(UX>2|U9?1(ZPVdG-MH{*u2?o#0%NjYA?cAs**&x5|4$u>u2dQ+wHV-L z{3?xTFezkL;`AyzFUH{YA}ebhJ``4+&8i9ksx&04vZ%>Ba>{z72I+9weB&B4 zr5gUK=4h&&uuccIhg%*(fg19`eRnf`HNfO&;UOfz!%EBq33XfqJ$+qvv}6_=$t&$9 zG)U;!`vB%`nQEn)IAnll#Ob(9z>o{v3)c-?ad#E1dbWkO(q+%EYwp!GRF;=)|5;Z2 zDy_b;5p~_MF1hh$?FsYtO8!exx{66dL`7#Rr%M!QwAj=ud80?~MvV?clw-b=3)mr5 zEIv?93J`W2nw>!N!2)6o4MO-pYNv9Tth#4El*!w1oT8G*0Ju}4J3{r~3aIL*$`FJY zav@Wd5I;ePtpF;f4I!-Dm~B*-#U23#P(tWF)yj~@qhc&7nA?w%#3GK6_ODJ0IRinB zqC~WHmn?-P&wmoNNb}m3Bem-Kkr~(PY>_@5)8KIuCIJgGp+cpPVyaKG@ZCB)1@w21 zfPZzax=6rCB(yd{jtoJG$V^L9nAi$vPXlf_6#dvOqVw}4xd7J`ilGG3Yt&T*;5$2U zRmJ>+zF;UvHmkJQ&<7%-XG_53!;d*&+uaAlUI!hpCXKjoWhWB9RpW?}iFljU7F?qR z_SLx{c97LI1QLm2H8_~Tir0Mz!UWs=-@5-E63i;TU7ZJ|t`u_dLF!mh{w6ezjd_9w zai{WZD9|uq2Vw#c2ksbBZ`qSDMdV^v0Y!sSgX6%D3puETA;7kzMT_r&F@gn(#5If3 zrzP1TgC#d4?OMQPk?^lduQWs>--Nj9hVWm9ioSMRIQUJDQ!9&Tk&$%Xy7Sh_PYIM6E>PMg1Ri2a; z*gvd)DlXU%GZW{gRQu14({dy!)p`KCV?d~_cgkjHz+t$YRbZ3|sfb>FbYd*p*y6&W zO#R!sDfgYTT~NI-KGpx??@3Ay(G7-}9z*FdL+y)M+ZQV{#)k5ihb~AB7xX;dGVVS2 z_i?M;p0edV4-!saHn6#}Y|S;$uS%1<7Bg}+cce0Hr1A1db0Z_=tWM$Y>M3~xMk+herfV{H5{M{pRJn0^dNJnQR;m$8^WZc5-#Nryr06K+6RUjN6)mZC9apekg9~}k z2R7pH_X9u=f}0U1$B-vVlAds&0HwkB#Tk)=vn!BFU{?`99!l;2WM^ET7QZjQDo6{+ zs#QAm2KMOnFUF8_Rzp|+ZsriAVd!pSSi)>c(P_T>p(*a!sY=h8tPDhy9!wkC+_nUD zAe|Q($C-0oQd~%Y&=Y2}0@BgICHUT7YfqlQ>A{zP(veC_N(r10TXncS7 zjMs0E{{*vo9}enCWKEVPKczVC7PyI2;dYz2j#2p?JGphJI}lkul&f?UwbjHE>eT^kVr>E2`xwp%fWnYc|87QUvx$EV{uWFvEc{4Ap zmj2`>4J~W}^eVstKZD9dAaXag618K$KCHyAt@~_8p+5ccX_t&`s;agy^>fNZ5(I;M z`Q#0VK|;bvurMx!fDIIp*%BnR+xK4fya$=F`O*}I77dX^Qdjr{1}qZnSKiJo?>%7p zF6zrWv+;SyiOHiAkN?EZ$A|X%^(8{$R^MzI+Rrt>n0LfP~JYGji@FK)l zTbJ|9+zxiNweAy6YvjK6xmAeK3Y0iln2Una;&sxmW6nN<8Bl=L;GDE!Es&!wMXJgC zkof_9ObV#K^`&nuZ;p|&SgaK3v??6?P^w+a2|ePDfFCh1$_48b^oFf zsCE5Z!r=|@<}ZW2F$#tpe}~R9w9X%^vyCGqWzJWptiJXpUtX*iRya&78raWYl&TC= z6DYYoLNayri*-Bi&Y`UMvgH-=2LI{rRZJXxTIjLncw)`@!WwjR_G^DS zEh6OB{X(uCFkBC~wSKPJEY4)Tc=BqJrbXxj^At_Xh<6sb_Un-sHqw8Z`$H`}e{Q7j z+cXrJF7QL>(&Gt_WAML@0%Z?znAve=6Ihj z@c!Lv@;mFh)$^X;3zMV#FxXiEsQ<0=jecYEA2xAAK@yzl8Q}<%5ind)`kK3fcX(C!+TmOg*2Y ze2#eb=8NsRu~t#&&(DzW4$J>M!=mubZR+Ef&3<^P@ATU;6OKrb3~wJ$i&W5{MyXT5 ziv_Pyy(&mJau4P<+VnBEPh3nBe|dl}y)9jAZz+|3B(&c!!8qDx$S7x}_8Y!(=l zE;WPI=tn3$&03=0lCHyYmF>(nPfal6KPcyF^&lVeGC4{K4pONFcF-IV zTY4Ss$yy&o8iTaw6NWY!XWUs5GCxN#Iw}FAU&j`DWx$Bnp0TlZ;eHph9KHcKqyWNz#@b z2!X}nG_B|AxiAPnt`e4af0MyMVCrd_Ez_w@2J)|cQ8yLdMNjO!L^Va#UA)`1=)}E& z)zeeUvto+Zr7oY;Q^{Fo>)Z)fXW8v$D?{Nt4^w*=I(!n#q>CZsk)a48W?FD_cFZ(! z`*&Xo6^fotOyDX>?pT8(W)~oy>f68eiJ_^bVTeCwq&w--+IzU&e-20DGGvk}KEmOt zn>=Y3RGcPE7sD0F@l-*TB+LWdpNSLw%rcB3ICkQ?5hN}`7cXFlI~tX1Zj+Nlk^x6g z0=Tj}HTzOaGf%~BNbS5-A+??@g?296jO}o} zd4$ywa=svUsYMD+rRKW{Us>BR5T;%<`sdWr>TEUn(oiPm9F6<}#IJLc zW~K~_Xb7tN3PL$(`MA#gOqcu5S%`Cy^iGR9&60HFLE-xsh@{wCJ(h}SJW$kX&lN+n zOPS_*`L5n4BziFZc^3Mmtj&Cmfz+Qs@_hBGN>tr3*C$Pl65H769EokZr6MRjXW6Fy$67;1lXxJLLlG(e8`_T?OrZ|BX>?>fvv=u<|3KmWd=4awbYy|zGm}UbF zc;*L@xJ^UC7x*gTICGh(>yF*-J{vRBPG21_xDu<-#pTXDs=9`{h{e(CZR*VzUqvWD^L%o~Ss z$5_3WfHu9srH{UeoSt)T=_y;VY!wA%o0OYBTv>k)*@fASh`UWzdY2E#@CA6%I`)#> zxau~m)GK<0b9g+a!R1|9fncJj8dY5yEQs`;ZBFM}j)aluG@A;{`~{Oeh1zmpOR?!( zUAcUtTXxWt7H+-Ht!9BzR_-O^7O~Q+;3O z`MY$oN-D9ns3zv<&NG2y5eM2U=3|cS`4h;;TyKR8>V}`TdUapX>`oK>Ms)CK(91`P zciYd#?mGHsW=1%57d0Jw+^_rfTcP5;L-3o&Gu>Zjz|3z9dVV{Z=RW&+7yi=Vn*|@4 z*9SU`l{#Oa`WD+edgysj((Qj~F<#!?)4W>$-ld}_&b;|^cysl*M3SSf>ryvr>+QFs zAM>|t27~uK10~wPJIhXg&Ioj3A8F-IDKW!f6Iy3UPVhJ(@jLb})#2Rxe*IU(Ky=x# zXJOl(?OtT+u4m)!`Oo^~ZxX#t|57FVU*0!)!mv7)<_r65;a2i|g66(0aM&wpAkLs* z$F`ka_Z<&!^A=8m(g^D`(sbDmuNp~(G7IMnIHvwd zRQ7>_p=CcGJ(9QNUxQk(_%nQKatuD9v+%nmnxBNEOQi8=i4|PLt|@lu;gu^&QXg!k zTsPLS!sKFQe7wj#q$+95mV5tWeW!Avo60vVc&O^@v+OYY?Z?JOcpA{Vln z@q@ihgeI7V50eu*I9 zRd`@jt!4Bo2tbj>3a{8bN`SJtvdpTuitQvI!Ns`+-jl}CdaP+Znq`Oiz0<|DCP!-bxSq$>_xQ)1J}n>NNVfHFpI zzCr^hfD}yvd*v8Lo8|@9MPG9O=@u0f;lWD*eL~+td6mtWrs3-Wbr(<$U9i;-RT~2f z1nlix;MZ$IhsvY1^*K5v^*tsktZg%*9V>Gb&Bnoq zjpi^zpzUav`v4DzT9MM=VfRJR&@Lb@@{v3hSmkdgT5FBPG120ca?wDH@IN}hp{eB3 zf+rpGhoyX@X}VVGQTi7YSN7P;L3UaeuPDm^pbXG;54u9w)Rj?QmL9Zsmb8MoMgT#y;=X3fTDT=QTGjvOR>$PkcB5-^ zHAfd)56BZI8G`|dtqvHTq{m?TmRo5{v-c6%@|4Dn1?ii{qir^$@p_||C{>m%_yIkg zRb>9X+l|tb?j^{4v(CmggNk;Q+Vo8toLd&fXk-xpA=t6&ts7g0Q}wCs<^yB4I)xVB zZ&;wwFh1qsp;@pmre5N59eCYf-=wDha$WPJ4&J>`gsI_|uCz?+3pyQdChrbREfrtt zO2cWfKa8gnT8n`v>}UzWa+g}9QAxdarf44&`{cdBn7m0&Z&01_k-8-t-7`LIFKmpz zhv#UU)UUvveq*5yj5Vl7F{?Ug>oML~>eF9|-kYY9`r^dK#>Z`ib vSN5xq<{@9%Q$E^fpDCQBN%d0nYUp~seE7I8CJxwjie|=Us8Z=P0I>dFkf?f- diff --git a/doc/img/home.png b/doc/img/home.png new file mode 100644 index 0000000000000000000000000000000000000000..48c46ce67232a280a01ba930ad8c83f01a03f6b1 GIT binary patch literal 10806 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 Vu(5x{QIyxRfYtO=(aO)l{udcT33UJf literal 20808 zcmeFZWmuG5)HX~hh$tnXQX`0nlz?;_pi(L+sURUO-Gdl3h!WCBN=r8*(%l_H4-G@i zzzhuU9=z}S`SU)%zF*&Qyl|k1!*%Uz?-l1d*SRL}nX2L?^6TUT1O%6!Jbw6sfPnlr z0Rf>k84>siy3+lCfZ#dhlZWyeZWEi+fi?QW?wo5lAqrJvYsAdW&C(|W=&HmYiDI)Y z>c)(}o)~X5UA?P9!2N#gM(BsRTPhy{F5PNTP#{yGp!q8QnCQmMk0dl-`=ok_2m0O8 z)7-_rK1I{EJ||0gF>F|an$bk$BfQ<1NRubZT!(WE*0K3^Cs_?K$IM_U_>aEc6V9x^ z-x3f|LH_-YgzVq%2=R-?f5`cs{RRH`e=j@RiT}4De+}aQS;*Nw{Qul#Qs_P#h|C_z zVsm3~YrVOz21ZZkV>=6>8v}X<4B!F(o^nv|<-FPj+AM$Q?0&$-J4-8R40R6EQG8Sc z1il1>_$Pu57uZuuOn7hL)8;=K1Q+@JAHfv z{d@b4(w%a(#N091))u^bht%I|&2F89OaY42>kU-75Fdpv+CfvZYY}R$<&jAy%du&x2?RwJTaG2@6jN zd7!9+E_PI&f08rNIWiJ5U$(k~mLO}+IJb3nRmk7~y`4ny3oS!eKUAbs3 zB-@YUtuf&8{i9$_g1^~5K||4rzYr;znatf0agwv_;?P%Dp6jwU)ILKyKBSryG}CsD zb#I`5&??K4t97c*ELVwNj^N`vQRyMiMV;ljd#G#gE{BfccXiRCpH^Mn*6AX>rXE(| zhTLCEbBffYEpZK$m3%35GLto7Vz>%txg~Lq%vVNzn}tO{Fj<(r9Bll10{KVjQ>T%; zceU`Rf&X%vxYymE^dWHXvvj-*eT>`pFL7=GZ?oNo`RWJ=sSGjxtt4b2V?loSlOiH% zA;&*630cB9f?0;>5D($H3k_gfd1s#~8otxso-+v~t#Rt2U@-6nk9pUao16`tJw)#8 zw1H*(#&!)c&D6=hTeCkMVY7T}b*nDcEur659!p3_B||Mn}4c(R(irU|+b!a-;EC54GY4qO*p?(9L&oD!^E&j0&CUrDwK znQF43gono#smR)zr|LnHvb8J~FTK9;|MIk##?d1MP2x>y(xfJ9oba>>L37jj=gec} zt<7{0umQ#bMP9ksCV5sV*kURb}jZ+U!lz)GEJV?*ID+rCjh7 zwN*<{2ysv5EW~8hhGkhbOf)S#htvEowp)ryCMFv5;Tp6vd~lNTyVC#i z`Y+8}!+JF4G@g{q%qKDEOxAYPqjlQwH`^E+zMlky5V==PT2~(W%7M?b_yR1-muBsW zcepcuZ|~e))mzL8n;JHlI<=}4A`%cTsP徴)@wSIQH^GT2YxVgz^MPrtt@=X0 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 >bhZ1WrTbyDH&kw-mW_B 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 diff --git a/front/src/img/favicon.png b/front/src/img/favicon.png index 0a5a1942294a6e3d8196ea49b61b29c2b2a48206..23ff5bc5698141afb0568fad80dd13f5b12f825b 100644 GIT binary patch delta 600 zcmV-e0;m0&2Id5i8Gix*008_L?V|ty0xd~IK~#7F?UVzvBS92K-_M9OiYB&=wQbwB zZQHhOtF*RlTes>l#>{%FPT14mY2D5did;XG6aoTw_OGrQQHP96|LRIo;-Dme^l$T- ziTFC7*|@l#k^sk?tx9XJjOIh!sJdjw@@ims9rEKG)`@{4Xn&FtPD+AYj`T-B5Hg!{ zfBFoTce7Y4+Y`Az|9ruJ0fM;Bj}(9)Xet!EoyFeL4W~y?Ct!f|Q$s~ibh-aPaorfp z|1gJD-DjjKfMX8PAgCQM6#XBfKg?#`9LHBmsa5jCMkJ)47>I~snpU(ILsR!k>g)wN z(*Ae~_u*u&T7MR2m|lYwR_WCYw%=&UMu1_?t=(`Ku9G;oiwH?2Xj(!Ck7fsKMsbTJh2YF^Vb%iR$&~1#$%)-tiVUfxj4yEOt*>XY zsSDoEW*e3EID!pb2xz8-ojoxObw&uvIZV(%7jI^=Z5msIV0`52QwOf82*Khu5_$zQ zo5>;sJy9yFX-tPJwn11QA;2-`uAUfOg2Vk#2#gTq-o5S0YL>Q@?Cyo_?}P5|gJcSH mT;5LHwyah{I1^Pe{d@;O2)u^ea1Uny0000;|EUzxcSE>cT7Tq;GTeeVrc7$OUs>&v zDg2}u`JEs9pd9_78~maf`=1@tctW*WUD$j*u~%Twbw$HyQ00p+vRGgKrWW3UIHO2z z^Oz&@mn61aT<44~l{P-mx1YQbhwl{6o*nO#C;z4w>5eVqh%%HqeDIVf*?v2uNp19+BJq|b|D_oHqZzbX zUaC)L_?{k}Lv!4LImBsCwp(2Oq!{#=Bgt+{;f6K;r+*g6ZA+p^ZuOcX;e|EznIhnX zHPd%OyI@!DlqmR|ANHFd=#DK3SnSyV00BcuL_t(|UbT_ak_15zMT^a$jkRstwr$(i ze{aO>^q{*sV)tRsYo5B9l?na=QqU1>)*Q2+|(zj22K=cMoGzsCWOs8jYp{gF_5~42KM{WML%Tt+|G2YYj9h|4Qj!G!#4Mt_|0-nzcAxs??sup{o$lz=oDN?a0$ z@aQ6+N%TNP%5ucM@UO_F=wFfC3Ztoxi`92nb9v*p?z&+8J;C*Ha z@(WdFcy&s=22OfY1Y}Qp|1hig{4ykVi3EGZqQV1GR%t^Tz%Tg*Q4=h%cVsO(00000 MNe4wvM6N<$f*e+NSO5S3 diff --git a/front/src/img/logo-large.png b/front/src/img/logo-large.png index f739504057983a007144315515a0a431463cce2e..88d1971904420a8d232c29ff14c8168d2e128525 100644 GIT binary patch literal 14666 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 j24#$!nTP)`_VUv= literal 4193 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 Date: Tue, 6 Oct 2020 00:55:29 +0200 Subject: [PATCH 22/59] Increase timeout to 2 minutes --- lib/tools/phantomas/phantomasWrapper.js | 40 +------------------------ package.json | 2 +- 2 files changed, 2 insertions(+), 40 deletions(-) diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index 308c0c5..21af510 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -31,7 +31,7 @@ var PhantomasWrapper = function() { var options = { // Cusomizable options - 'timeout': task.options.timeout || 60, + 'timeout': task.options.timeout || 120, 'user-agent': (task.options.device === 'desktop') ? 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/537.36 (KHTML, like Gecko) YLT Chrome/85.0.4183.121 Safari/537.36' : null, 'tablet': (task.options.device === 'tablet'), 'phone': (task.options.device === 'phone'), @@ -79,44 +79,6 @@ var PhantomasWrapper = function() { deferred.reject('Phantomas failed: ' + res.message); }); - /*var process = phantomas(task.url, options, function(err, json, results) { - var errorCode = err ? parseInt(err.message, 10) : null; - - debug('Returning from Phantomas with error %s', errorCode); - - // Adding some YellowLabTools errors here - if (json && json.metrics && (!json.metrics.javascriptExecutionTree || !json.offenders.javascriptExecutionTree)) { - errorCode = 1001; - } - - if (!errorCode && (!json || !json.metrics)) { - errorCode = 1002; - } - - // Don't cancel test if it is a timeout and we've got some results - if (errorCode === 252 && json) { - debug('Timeout after ' + options.timeout + ' seconds. But it\'s not a problem, the test is valid.'); - errorCode = null; - } - - if (errorCode) { - debug('Attempt failed. Error code ' + errorCode); - } - - }, function(err, json) { - - if (err) { - debug('All ' + triesNumber + ' attemps failed for the test'); - deferred.reject(err); - - } else { - - deferred.resolve(json); - - } - }); - */ - return deferred.promise; }; }; diff --git a/package.json b/package.json index 9162731..c562173 100644 --- a/package.json +++ b/package.json @@ -62,7 +62,7 @@ "meow": "5.0.0", "minimize": "2.2.0", "parse-color": "1.0.0", - "phantomas": "git://github.com/macbre/phantomas.git#phantomas-v2", + "phantomas": "git://github.com/macbre/phantomas.git#devel", "ps-node": "0.1.6", "q": "1.5.1", "request": "2.88.0", From 8586d1cc12d26546a0b2e57dddb29ea548abf784 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 6 Oct 2020 00:58:36 +0200 Subject: [PATCH 23/59] Documentation --- doc/img/logo.png | Bin 8792 -> 30295 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/img/logo.png b/doc/img/logo.png index e505728ff97da14898b2664b736821980b9e59da..b9425f5144df15fc3de4f01122d529378b010a4b 100644 GIT binary patch literal 30295 zcmeFZ^ie!#XP+(ZevXEn<;`VN8) zeYMzqPd$xSQJM4)!9CA^t|)MBoZlb(c5G9`7TFv`xw}X|821U%Z2|B--YLx z;EB`-`w~)oH4(pI&D~6!FgimmmtA&{c*9up5(J_`*?W#5yr)rbdnz=PI=DC3ANmU8 z`ctL#wKTxj??(NLBAgx0#-`NB1yGgh)>^UPs{j4FL7^h|1|y-P5oBwtO;sA9>Z$a4}@NWK#ud|mM z&aVrw-2V??(rbGy8gP_e$qv12Be`l5Cx;`Jqhdsqr5|IK+dhj7BR(u!k*DFH6^kz5 zHt~cEyf4(vG3^_or2NHKk0rEq&ETJGQ(cHQfNj1#+$>f8K6yYOLDZe~!WVQYAjPmd zAl{-wgQz${?t4lNXV%r#J!}bT4j}tHRiQSVnW;!eao-+hw|?o#%UEvuLb^d7iy;jT zb*FVl36HJ|PsWs>^+VxJ1JEdqg44OUk3^~!cMx!iAh4Vnb8`4CM)UPmEcNwga@C-J zOH-27^3=<%r?oH8H{g!~P^h}9kxQGu*GG!<%H^ZW>mp|bFVbdWyrR%Jq1#6O@jH$favIG7)w})ju^bGjX|xbX7A6exQ4!ygTB6($NB|t zd+xNY%;C!Iеkhx{Xg|; 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 U6ggiR-Z$&(8eh)WzV-Nj0jdR)0{{R3 literal 8792 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 Vu(5x{QIyxRfYtO=(aO)l{udcT33UJf From 7bc994b99f540a2147b1fef245b2c98dad6cf96a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 6 Oct 2020 00:59:14 +0200 Subject: [PATCH 24/59] Documentation --- doc/img/logo.png | Bin 30295 -> 9391 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/doc/img/logo.png b/doc/img/logo.png index b9425f5144df15fc3de4f01122d529378b010a4b..cc0042b53398b4c6eb22c12ea78a53149f4a0c4f 100644 GIT binary patch literal 9391 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еkhx{Xg|; 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 U6ggiR-Z$&(8eh)WzV-Nj0jdR)0{{R3 From 48fbb1076fa813a43a441672129de4a48dfdcca4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 6 Oct 2020 08:30:48 +0200 Subject: [PATCH 25/59] Adjust design --- front/src/css/dashboard.css | 4 ++-- front/src/less/dashboard.less | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/front/src/css/dashboard.css b/front/src/css/dashboard.css index 78313eb..2808d86 100644 --- a/front/src/css/dashboard.css +++ b/front/src/css/dashboard.css @@ -82,11 +82,11 @@ } } .summary .notations .category { - font-weight: bold; + font-size: 1.2em; width: 50%; float: left; text-align: left; - margin: 1em 0.25em; + margin: 0.5em 0.25em; } @media (min-width: 820px) { .summary .notations .category { diff --git a/front/src/less/dashboard.less b/front/src/less/dashboard.less index 9f2ca51..6fd856f 100644 --- a/front/src/less/dashboard.less +++ b/front/src/less/dashboard.less @@ -88,11 +88,11 @@ } } .summary .notations .category { - font-weight: bold; + font-size: 1.2em; width: 50%; float: left; text-align: left; - margin: 1em 0.25em; + margin: 0.5em 0.25em; @media (min-width: 820px) { width: 20%; From 693fce1bb1ab97f6256a26feaa83f793d5c8004e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 10 Nov 2020 15:00:02 +0100 Subject: [PATCH 26/59] Flag as alpha version --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index c562173..4cd0bb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "yellowlabtools", - "version": "1.13.3", + "version": "2.0.0-alpha", "description": "Online tool to audit a webpage for performance and front-end quality issues", "license": "GPL-2.0", "author": { @@ -62,7 +62,7 @@ "meow": "5.0.0", "minimize": "2.2.0", "parse-color": "1.0.0", - "phantomas": "git://github.com/macbre/phantomas.git#devel", + "phantomas": "2.0.0", "ps-node": "0.1.6", "q": "1.5.1", "request": "2.88.0", From 5bf3bd7ce5a980895d5e925784fc5dadfc4c4d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 23 Nov 2020 13:49:04 +0100 Subject: [PATCH 27/59] Fix a potential divided by O error --- lib/screenshotHandler.js | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/screenshotHandler.js b/lib/screenshotHandler.js index 6c9629f..29d68a8 100644 --- a/lib/screenshotHandler.js +++ b/lib/screenshotHandler.js @@ -54,19 +54,24 @@ var screenshotHandler = function() { var deferred = Q.defer(); var currentWidth = image.bitmap.width; - var ratio = newWidth / currentWidth; - image.scale(ratio, function(err, image){ - if (err) { - debug('Could not resize image'); - debug(err); + if (currentWidth > 0) { + var ratio = newWidth / currentWidth; - deferred.reject(err); - } else { - debug('Image correctly resized'); - deferred.resolve(image); - } - }); + image.scale(ratio, function(err, image){ + if (err) { + debug('Could not resize image'); + debug(err); + + deferred.reject(err); + } else { + debug('Image correctly resized'); + deferred.resolve(image); + } + }); + } else { + deferred.reject('Could not resize an empty image'); + } return deferred.promise; }; From ffd9efeb986c35fd6e89ee9d2e175f5914b12588 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 24 Nov 2020 00:18:55 +0100 Subject: [PATCH 28/59] New DOMaccess agregated form + new score category JS complexity --- .../src/js/directives/offendersDirectives.js | 7 +++ front/src/views/rule.html | 32 ++++++++++- lib/metadata/policies.js | 39 +++----------- lib/metadata/scoreProfileGeneric.json | 10 +++- lib/runner.js | 4 ++ lib/tools/domAccessAgregator.js | 53 +++++++++++++++++++ 6 files changed, 112 insertions(+), 33 deletions(-) create mode 100644 lib/tools/domAccessAgregator.js diff --git a/front/src/js/directives/offendersDirectives.js b/front/src/js/directives/offendersDirectives.js index c2663bc..956a9cb 100644 --- a/front/src/js/directives/offendersDirectives.js +++ b/front/src/js/directives/offendersDirectives.js @@ -93,6 +93,13 @@ }; }); + offendersDirectives.filter('lastDOMNode', function() { + return function(str) { + var splited = str.split(' > '); + return splited[splited.length - 1]; + }; + }); + function getBacktraceHTML(backtrace) { var html = ''; var parsedBacktrace = parseBacktrace(backtrace); diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 58a3c30..73ce778 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -30,7 +30,7 @@

This rule reached the abnormality threshold, which means there is a real problem you should care about.

-

+

@@ -264,6 +264,36 @@
+
+
+

+ from + getElementById() + getElementsByTagName() + getElementsByClassName() + querySelector() or querySelectorAll() + appendChild() or insertBefore() + added nodes + removed nodes + attribute changes + addEventListener() +

+
+
+
#{{access.id}}
+
{{access.tag}} on {{access.node | lastDOMNode}}
+
.{{access.class}} on {{access.node | lastDOMNode}}
+
{{access.selector}} on {{access.node | lastDOMNode}}
+
{{access.append | lastDOMNode}} added to {{access.node | lastDOMNode}}
+
{{access.node}} added to {{access.target}}
+
{{access.node}} removed from {{access.target}}
+
{{access.attribute}} changed on {{access.node}}
+
{{access.eventType}} on {{access.path | lastDOMNode}}
+
+
+
+
+

{{rule.value | bytes}} could be saved on

diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index a564345..49bf3a4 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -46,37 +46,14 @@ var policies = { "isAbnormalThreshold": 50, "hasOffenders": true }, - "DOMqueriesAvoidable": { - "tool": "phantomas", - "label": "Duplicated DOM queries", - "message": "

This is the number of queries that could be avoided by removing all duplicated queries.

Simply save the result of a query in a variable. Ok it is not always simple, especially with third-party scripts, but at least do it with your own code.

", - "isOkThreshold": 0, - "isBadThreshold": 300, - "isAbnormalThreshold": 600, - "hasOffenders": true, - "takeOffendersFrom": "DOMqueriesDuplicated", - "offendersTransformFn": function(offenders) { - return { - count: offenders.length, - list: offenders.map(function(offender) { - var parts = /^[^"]* ?"(.*)" ?with ?(.*) ?\(in ?context ?(.*)\): ?(.*)\s?queries$/.exec(offender); - - if (!parts) { - debug('DOMqueriesAvoidable offenders transform function error with "%s"', offender); - return { - parseError: offender - }; - } - - return { - query: parts[1], - context: offendersHelpers.domPathToDomElementObj(parts[3]), - fn: parts[2], - count: parseInt(parts[4], 10) - }; - }) - }; - } + "DOMaccesses": { + "tool": "domAccessAgregator", + "label": "DOM access", + "message": "

This metric estimates the number of times the JavaScript reads, changes or binds the DOM.

The more your JavaScript code accesses the DOM, the slower the page will load.

Try, as much as possible, to have an HTML page fully generated by the server instead of making changes with JS.

Try to reduce the number of queries by refactoring your JavaScript code.

Binding too many events also has a cost.

", + "isOkThreshold": 500, + "isBadThreshold": 2500, + "isAbnormalThreshold": 5000, + "hasOffenders": true }, "eventsScrollBound": { "tool": "phantomas", diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 5646b26..f9aa7a8 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -32,8 +32,15 @@ "DOMidDuplicated": 1 } }, + "javascriptComplexity": { + "label": "JS complexity", + "policies": { + "DOMaccesses": 4, + "eventsScrollBound": 1 + } + }, "badJavascript": { - "label": "Bad JavaScript", + "label": "Bad JS", "policies": { "jsErrors": 1, "documentWriteCalls": 2, @@ -100,6 +107,7 @@ "pageWeight": 3, "requests": 3, "domComplexity": 2, + "javascriptComplexity": 2, "badJavascript": 2, "jQuery": 1, "cssSyntaxError": 1, diff --git a/lib/runner.js b/lib/runner.js index c201302..f0ba25b 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -3,6 +3,7 @@ var debug = require('debug')('ylt:runner'); var phantomasWrapper = require('./tools/phantomas/phantomasWrapper'); var colorDiff = require('./tools/colorDiff'); +var domAccessAgregator = require('./tools/domAccessAgregator'); var mediaQueriesChecker = require('./tools/mediaQueriesChecker'); var isHttp2 = require('./tools/isHttp2'); var redownload = require('./tools/redownload/redownload'); @@ -27,6 +28,9 @@ var Runner = function(params) { .then(function(phantomasResults) { data.toolsResults.phantomas = phantomasResults; + // Mix all DOM Access metrics together + data = domAccessAgregator.agregate(data); + // Compare colors data = colorDiff.compareAllColors(data); diff --git a/lib/tools/domAccessAgregator.js b/lib/tools/domAccessAgregator.js new file mode 100644 index 0000000..01a5472 --- /dev/null +++ b/lib/tools/domAccessAgregator.js @@ -0,0 +1,53 @@ +var debug = require('debug')('ylt:domAccessAgregator'); + +var domAccessAgregator = function() { + 'use strict'; + + this.agregate = function(data) { + debug('Starting to agregate DOM Accesses...'); + + let count = 0; + let offenders = { + byType: {} + }; + + const metricsToGather = [ + 'DOMqueriesById', + 'DOMqueriesByTagName', + 'DOMqueriesByClassName', + 'DOMqueriesByQuerySelectorAll', + 'DOMinserts', + 'DOMmutationsInserts', + 'DOMmutationsRemoves', + 'DOMmutationsAttributes', + 'eventsBound' + ]; + + metricsToGather.forEach(key => { + + if (data.toolsResults.phantomas.metrics[key]) { + count += data.toolsResults.phantomas.metrics[key]; + } + + offenders.byType[key] = []; + if (data.toolsResults.phantomas.offenders[key]) { + offenders.byType[key] = data.toolsResults.phantomas.offenders[key]; + } + }); + + data.toolsResults.domAccessAgregator = { + metrics: { + DOMaccesses: count + }, + offenders: { + DOMaccesses: offenders + } + }; + + debug('Done agregating DOM Accesses.'); + + return data; + }; +}; + +module.exports = new domAccessAgregator(); \ No newline at end of file From 9aea4e567778fadd8f76814ce8a15bea19c0560d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Thu, 26 Nov 2020 01:37:29 +0100 Subject: [PATCH 29/59] New metrics: old HTTP and TLS protocols --- front/src/views/rule.html | 8 ++ lib/metadata/policies.js | 56 +++++++++----- lib/metadata/scoreProfileGeneric.json | 8 +- lib/rulesChecker.js | 3 +- lib/runner.js | 6 -- lib/tools/isHttp2.js | 105 -------------------------- package.json | 3 +- 7 files changed, 52 insertions(+), 137 deletions(-) delete mode 100644 lib/tools/isHttp2.js diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 73ce778..0cf9911 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -175,6 +175,14 @@ ({{offender.size | bytes}})
+ +
+ {{offender.domain}} receives over {{offender.httpVersion}} +
+ +
+ {{offender.domain}} uses {{offender.tlsVersion}} and seems to be on the critical path +
diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 49bf3a4..befe3df 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -927,35 +927,55 @@ var policies = { return offenders; } },*/ - "http2": { - "label": "HTTP/2 or SPDY", - "message": "

HTTP/2 is the latest version of the HTTP protocol and is designed to optimize load speed. SPDY is deprecated but still very well supported.

The latest versions of all major browsers are now compatible. The difficulty is on the server side, where technologies are not quite ready yet.

", + "oldHttpProtocol": { + "label": "HTTP/1 requests", + "message": "

HTTP/2 is the latest version of the HTTP protocol. It is designed to optimize load speed. HTTP/3 will come soon and should be even faster!

When a domain sends more than 4 requests over HTTP/1, this metric counts one point for each new request. Below 5 requests, the benefits of HTTP/2 are generally less significant.

", "hasOffenders": true, "scoreFn": function(data) { - if (!data.toolsResults.http2) { - return null; + var count = 0; + + var offenders = data.toolsResults.phantomas.offenders.oldHttpProtocol; + + if (offenders) { + offenders.forEach(function(offender) { + if (offender.requests > 4) { + count += offender.requests - 4; + } + }); } - var isHttp2 = data.toolsResults.http2.metrics.http2; + var isOkThreshold = 0; + var isBadThreshold = 25; + var isAbnormalThreshold = 100; + var score = (isBadThreshold - count) * 100 / (isBadThreshold - isOkThreshold); + var abnormalityScore = (isAbnormalThreshold - count) * 100 / (isAbnormalThreshold - isOkThreshold); + + var result = { - value: isHttp2 ? 'Yes' : 'No', - score: isHttp2 ? 100 : 0, - bad: !isHttp2, - abnormal: false, - abnormalityScore: 0 + value: count, + score: Math.min(Math.max(Math.round(score), 0), 100), + bad: count > isBadThreshold, + abnormal: count > isAbnormalThreshold, + abnormalityScore: Math.min(Math.round(abnormalityScore), 0), + offendersObj: { + count: data.toolsResults.phantomas.metrics.oldHttpProtocol, + list: data.toolsResults.phantomas.offenders.oldHttpProtocol || [] + } }; - if (data.toolsResults.http2.offenders) { - result.offendersObj = { - count: data.toolsResults.http2.offenders.http2.length, - list: data.toolsResults.http2.offenders.http2 - }; - } - return result; } }, + "oldTlsProtocol": { + "tool": "phantomas", + "label": "Old TLS protocols", + "message": "

Counts the number of domains that use TLS versions < 1.3. This is the latest version and it includes a faster \"handshake\" technology.

The 1.0 and 1.1 versions are deprecated because they are unsafe, switch to 1.2 or above as soon as possible.

The version 1.3 includes an even faster option called 0-RTT, check your server compatibility on SSL Labs.

", + "isOkThreshold": 0, + "isBadThreshold": 5, + "isAbnormalThreshold": 15, + "hasOffenders": true + }, "cachingDisabled": { "tool": "phantomas", "label": "Caching disabled", diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index f9aa7a8..ab4abae 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -53,9 +53,7 @@ "label": "jQuery", "policies": { "jQueryVersion": 2, - "jQueryVersionsLoaded": 2, - "jQueryCallsOnEmptyObject": 1, - "jQueryNotDelegatedEvents": 1 + "jQueryVersionsLoaded": 2 } }, "cssComplexity": { @@ -95,7 +93,8 @@ "serverConfig": { "label": "Server config", "policies": { - "http2": 2, + "oldHttpProtocol": 2, + "oldTlsProtocol": 2, "closedConnections": 2, "cachingNotSpecified": 1, "cachingDisabled": 1, @@ -110,7 +109,6 @@ "javascriptComplexity": 2, "badJavascript": 2, "jQuery": 1, - "cssSyntaxError": 1, "cssComplexity": 1, "badCSS": 1, "fonts": 1, diff --git a/lib/rulesChecker.js b/lib/rulesChecker.js index bc570a7..eaffd6e 100644 --- a/lib/rulesChecker.js +++ b/lib/rulesChecker.js @@ -111,7 +111,8 @@ var RulesChecker = function() { if (rule) { rule.policy = { label: policy.label, - message: policy.message + message: policy.message, + hasOffenders: policy.hasOffenders || false }; results[metricName] = rule; diff --git a/lib/runner.js b/lib/runner.js index f0ba25b..93255e8 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -5,7 +5,6 @@ var phantomasWrapper = require('./tools/phantomas/phantomasWrapper'); var colorDiff = require('./tools/colorDiff'); var domAccessAgregator = require('./tools/domAccessAgregator'); var mediaQueriesChecker = require('./tools/mediaQueriesChecker'); -var isHttp2 = require('./tools/isHttp2'); var redownload = require('./tools/redownload/redownload'); var rulesChecker = require('./rulesChecker'); var scoreCalculator = require('./scoreCalculator'); @@ -42,11 +41,6 @@ var Runner = function(params) { }) - .then(function(data) { - // Check if HTTP2 - return isHttp2.check(data); - }) - .then(function(data) { // Rules checker diff --git a/lib/tools/isHttp2.js b/lib/tools/isHttp2.js deleted file mode 100644 index acea838..0000000 --- a/lib/tools/isHttp2.js +++ /dev/null @@ -1,105 +0,0 @@ -var debug = require('debug')('ylt:isHttp2'); -var url = require('url'); -var Q = require('q'); -var http2 = require('is-http2'); - -var isHttp2 = function() { - 'use strict'; - - this.check = function(data) { - debug('Starting to check for HTTP2 support...'); - - return this.checkHttp2(data) - - .then(function(result) { - - if (result.isHttp) { - debug('The website is not even in HTTPS'); - - data.toolsResults.http2 = { - metrics: { - http2: false - } - }; - - } else if (result.isHttp2) { - debug('HTTP/2 (or SPDY) is supported'); - - data.toolsResults.http2 = { - metrics: { - http2: true - } - }; - - } else { - debug('HTTP/2 is not supported'); - - data.toolsResults.http2 = { - metrics: { - http2: false - } - }; - } - - // Add the supported protocols as offenders - if (result.supportedProtocols) { - debug('Supported protocols: ' + result.supportedProtocols.join(' ')); - data.toolsResults.http2.offenders = { - http2: result.supportedProtocols - }; - } - - debug('End of HTTP2 support check'); - - return data; - }) - - .fail(function() { - return data; - }); - }; - - this.getParsedUrl = function(data) { - return url.parse(data.toolsResults.phantomas.url); - }; - - this.getProtocol = function(data) { - return this.getParsedUrl(data).protocol; - }; - - this.getDomain = function(data) { - return this.getParsedUrl(data).hostname; - }; - - this.checkHttp2 = function(data) { - var deferred = Q.defer(); - - // Check if it's HTTPS first - if (this.getProtocol(data) === 'http:') { - - deferred.resolve({ - isHttp: true - }); - - } else { - - // To make is-http2 work, you need to have openssl in a version greater than 1.0.0 installed and available in your $path. - http2(this.getDomain(data), {includeSpdy: true}) - - .then(function(result) { - deferred.resolve(result); - }) - - .catch(function(error) { - debug('Error while checking HTTP2 support:'); - debug(error); - deferred.reject('Error while checking for HTTP2 support'); - }); - - } - - return deferred.promise; - }; -}; - -module.exports = new isHttp2(); \ No newline at end of file diff --git a/package.json b/package.json index 4cd0bb2..92ae9fb 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,6 @@ "imagemin-svgo": "7.0.0", "is-eot": "1.0.0", "is-gif": "3.0.0", - "is-http2": "1.2.0", "is-jpg": "2.0.0", "is-otf": "0.1.2", "is-png": "1.1.0", @@ -62,7 +61,7 @@ "meow": "5.0.0", "minimize": "2.2.0", "parse-color": "1.0.0", - "phantomas": "2.0.0", + "phantomas": "github:macbre/phantomas#devel", "ps-node": "0.1.6", "q": "1.5.1", "request": "2.88.0", From d334726991e63775540b3d96deecb9f5771c0b6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Thu, 26 Nov 2020 19:58:46 +0100 Subject: [PATCH 30/59] Fix offendersdisplay on jquery versions page --- front/src/views/rule.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 0cf9911..ee74fbd 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -168,7 +168,7 @@
- {{offender}} + {{offender.version}}
From dd0c17a9d4da414bc3b5ef74d3da6ac9a831fb6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sat, 19 Dec 2020 23:44:36 +0100 Subject: [PATCH 31/59] Fix content type checking after switching to phantomas v2 --- lib/tools/redownload/contentTypeChecker.js | 31 ++++++++++------------ 1 file changed, 14 insertions(+), 17 deletions(-) diff --git a/lib/tools/redownload/contentTypeChecker.js b/lib/tools/redownload/contentTypeChecker.js index a55a536..4d7d631 100644 --- a/lib/tools/redownload/contentTypeChecker.js +++ b/lib/tools/redownload/contentTypeChecker.js @@ -17,6 +17,7 @@ var ContentTypeChecker = function() { var deferred = Q.defer(); debug('Entering contentTypeChecker'); + debug(entry); // Setting isSomething values: switch(entry.type) { @@ -57,24 +58,10 @@ var ContentTypeChecker = function() { try { foundType = findContentType(entry.weightCheck.bodyBuffer); - - // Note: as of Phantomas v2, entry.contentType is always undefined - // It could change, so let's keep the following code in place: - if (!entry.contentType || entry.contentType === '') { - if (foundType === null) { - debug('ContentType is empty for file %s', entry.url); - } else { - debug('ContentType is empty for file %s. It should be %s.', entry.url, foundType.mimes[0]); - entry.oldContentType = null; - rewriteContentType(entry, foundType); - } - } else { - if (foundType !== null && foundType.mimes.indexOf(entry.contentType) === -1) { - debug('ContentType %s is wrong for %s. It should be %s.', entry.contentType, entry.url, foundType.mimes[0]); - entry.oldContentType = entry.contentType; - rewriteContentType(entry, foundType); - } + if (foundType !== null && foundType.type !== entry.type) { + debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.ulr, foundType.type); + rewriteContentType(entry, foundType); } } catch(err) { @@ -155,6 +142,7 @@ var ContentTypeChecker = function() { var contentTypes = { jpeg: { + type: 'image', mimes: ['image/jpeg'], updateFn: function(entry) { entry.type = 'image'; @@ -162,6 +150,7 @@ var ContentTypeChecker = function() { } }, png: { + type: 'image', mimes: ['image/png'], updateFn: function(entry) { entry.type = 'image'; @@ -169,6 +158,7 @@ var ContentTypeChecker = function() { } }, svg: { + type: 'image', mimes: ['image/svg+xml'], updateFn: function(entry) { entry.type = 'image'; @@ -177,6 +167,7 @@ var ContentTypeChecker = function() { } }, gif: { + type: 'image', mimes: ['image/gif'], updateFn: function(entry) { entry.type = 'image'; @@ -184,6 +175,7 @@ var ContentTypeChecker = function() { } }, webp: { + type: 'image', mimes: ['image/webp'], updateFn: function(entry) { entry.type = 'image'; @@ -191,6 +183,7 @@ var ContentTypeChecker = function() { } }, woff: { + type: 'webfont', mimes: ['application/x-font-woff', 'application/font-woff', 'font/woff'], updateFn: function(entry) { entry.type = 'webfont'; @@ -198,6 +191,7 @@ var ContentTypeChecker = function() { } }, woff2: { + type: 'webfont', mimes: ['font/woff2', 'application/x-font-woff2', 'application/font-woff2'], updateFn: function(entry) { entry.type = 'webfont'; @@ -205,6 +199,7 @@ var ContentTypeChecker = function() { } }, otf: { + type: 'webfont', mimes: ['application/x-font-otf', 'font/otf', 'font/opentype', 'application/x-font-opentype'], updateFn: function(entry) { entry.type = 'webfont'; @@ -212,6 +207,7 @@ var ContentTypeChecker = function() { } }, ttf: { + type: 'webfont', mimes: ['application/x-font-ttf', 'font/ttf', 'application/x-font-truetype'], updateFn: function(entry) { entry.type = 'webfont'; @@ -219,6 +215,7 @@ var ContentTypeChecker = function() { } }, eot: { + type: 'webfont', mimes: ['application/vnd.ms-fontobject', 'font/eot'], updateFn: function(entry) { entry.type = 'webfont'; From d93af3d6e4406398f2db2f72b90f7fa77a8f1ae0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 21 Dec 2020 01:18:56 +0100 Subject: [PATCH 32/59] Detect non WOFF2 fonts and estimate gain --- front/src/views/rule.html | 20 +++++ lib/metadata/policies.js | 10 +++ lib/metadata/scoreProfileGeneric.json | 4 +- lib/tools/redownload/contentTypeChecker.js | 18 +++-- lib/tools/redownload/fontAnalyzer.js | 87 ++++++++++++++++++++-- lib/tools/redownload/redownload.js | 40 ++++++++++ package.json | 4 +- 7 files changed, 170 insertions(+), 13 deletions(-) diff --git a/front/src/views/rule.html b/front/src/views/rule.html index ee74fbd..9b063a6 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -483,6 +483,26 @@
+
+

{{rule.value | bytes}} could be saved on

+
+
+
File
+
Current weight
+
Woff 2 weight
+
Gain
+
+
+
+ +
+
{{file.originalSize | bytes}}
+
{{file.woff2Size | bytes}}
+
-{{file.gain | bytes}}
+
+
+
+

Protocols advertised by the server

diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index befe3df..b6d7e3d 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -927,6 +927,16 @@ var policies = { return offenders; } },*/ + "nonWoff2Fonts": { + "tool": "redownload", + "label": "Woff 2", + "message": "

The fonts listed here could be lighter if they were served with the latest woff 2 font file format. Some online tools can help you easily convert older formats to woff 2.

", + "isOkThreshold": 0, + "isBadThreshold": 61440, + "isAbnormalThreshold": 122880, + "hasOffenders": true, + "unit": 'bytes' + }, "oldHttpProtocol": { "label": "HTTP/1 requests", "message": "

HTTP/2 is the latest version of the HTTP protocol. It is designed to optimize load speed. HTTP/3 will come soon and should be even faster!

When a domain sends more than 4 requests over HTTP/1, this metric counts one point for each new request. Below 5 requests, the benefits of HTTP/2 are generally less significant.

", diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index ab4abae..2ebd278 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -87,7 +87,9 @@ "label": "Web fonts", "policies": { "fontsCount": 1, - "heavyFonts": 1 + "heavyFonts": 0.5, + //"unusedUnicodeRanges": 0.5, + "nonWoff2Fonts": 0.5 } }, "serverConfig": { diff --git a/lib/tools/redownload/contentTypeChecker.js b/lib/tools/redownload/contentTypeChecker.js index 4d7d631..e83924f 100644 --- a/lib/tools/redownload/contentTypeChecker.js +++ b/lib/tools/redownload/contentTypeChecker.js @@ -15,9 +15,6 @@ var ContentTypeChecker = function() { function checkContentType(entry) { var deferred = Q.defer(); - - debug('Entering contentTypeChecker'); - debug(entry); // Setting isSomething values: switch(entry.type) { @@ -59,9 +56,15 @@ var ContentTypeChecker = function() { try { foundType = findContentType(entry.weightCheck.bodyBuffer); - if (foundType !== null && foundType.type !== entry.type) { - debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.ulr, foundType.type); - rewriteContentType(entry, foundType); + if (foundType !== null) { + if (foundType.type === 'webfont') { + // Always rewrite fonts for woff2 checking + rewriteContentType(entry, foundType); + } else if (foundType.type !== entry.type) { + // For other kind of files, just rewrite if needed + debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.ulr, foundType.type); + rewriteContentType(entry, foundType); + } } } catch(err) { @@ -188,6 +191,7 @@ var ContentTypeChecker = function() { updateFn: function(entry) { entry.type = 'webfont'; entry.isWebFont = true; + entry.isWoff = true; } }, woff2: { @@ -196,6 +200,7 @@ var ContentTypeChecker = function() { updateFn: function(entry) { entry.type = 'webfont'; entry.isWebFont = true; + entry.isWoff2 = true; } }, otf: { @@ -212,6 +217,7 @@ var ContentTypeChecker = function() { updateFn: function(entry) { entry.type = 'webfont'; entry.isWebFont = true; + entry.isTTF = true; } }, eot: { diff --git a/lib/tools/redownload/fontAnalyzer.js b/lib/tools/redownload/fontAnalyzer.js index 0fab2f5..3cddcfc 100644 --- a/lib/tools/redownload/fontAnalyzer.js +++ b/lib/tools/redownload/fontAnalyzer.js @@ -2,6 +2,8 @@ var debug = require('debug')('ylt:fontAnalyzer'); var Q = require('q'); var fontkit = require('fontkit'); +var woffTools = require('woff-tools'); +var ttf2woff2 = require('ttf2woff2'); var FontAnalyzer = function() { @@ -19,25 +21,100 @@ var FontAnalyzer = function() { if (entry.isWebFont) { debug('File %s is a font. Let\'s have a look inside!', entry.url); - getMetricsFromFont(entry, charsListOnPage) + convertToWoff2(entry) + + .then(function(entry) { + return getMetricsFromFont(entry, charsListOnPage); + }) .then(function(fontMetrics) { entry.fontMetrics = fontMetrics; - deferred.resolve(entry); }) .fail(function(error) { debug('Could not open the font: %s', error); - deferred.resolve(entry); }); - } else { - deferred.resolve(entry); } + + deferred.resolve(entry); return deferred.promise; } + function convertToWoff2(entry) { + var deferred = Q.defer(); + + debug('Entering font format converter...'); + + if (entry.isWoff2) { + + debug('File is already a woff2.'); + deferred.resolve(entry); + + } else if (entry.isWoff) { + + debug('File is a woff. Let\'s convert to woff2'); + + try { + + var fileSize = entry.weightCheck.bodySize; + debug('Current file size is %d', fileSize); + + var ttf = woffTools.toSfnt(entry.weightCheck.bodyBuffer); + var woff2 = ttf2woff2(ttf); + + var newFileSize = woff2.length; + + debug('New image size is %d', newFileSize); + debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize)); + entry.weightCheck.sizeAsWoff2 = newFileSize; + + deferred.resolve(entry); + + } catch(error) { + deferred.reject(error); + } + + } else if (entry.isTtf) { + + debug('File is a TTF. Let\'s convert to woff2'); + + try { + + var fileSize = entry.weightCheck.bodySize; + debug('Current file size is %d', fileSize); + + var woff2 = ttf2woff2(entry.weightCheck.bodyBuffer); + + var newFileSize = woff2.length; + + debug('New image size is %d', newFileSize); + debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize)); + entry.weightCheck.sizeAsWoff2 = newFileSize; + + deferred.resolve(entry); + + } catch(error) { + deferred.reject(error); + } + + } else { + // Other font formats are not handled + deferred.resolve(entry); + } + + return deferred.promise; + } + + // The gain is estimated of enough value if it's over 1KB or over 20%, + // but it's ignored if is below 100 bytes + function gainIsEnough(oldWeight, newWeight) { + var gain = oldWeight - newWeight; + var ratio = gain / oldWeight; + return (gain > 2048 || (ratio > 0.2 && gain > 100)); + } + function getMetricsFromFont(entry, charsListOnPage) { var deferred = Q.defer(); diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index d770df7..86927c4 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -167,6 +167,10 @@ var Redownload = function() { offenders.fontsCount = listFonts(results); metrics.fontsCount = offenders.fontsCount.count; + // Conversion to woff2 + offenders.nonWoff2Fonts = listNonWoff2Fonts(results); + metrics.nonWoff2Fonts = offenders.nonWoff2Fonts.totalGain; + // Heavy fonts offenders.heavyFonts = listHeavyFonts(results); metrics.heavyFonts = offenders.heavyFonts.totalGain; @@ -551,6 +555,42 @@ var Redownload = function() { }; } + function listNonWoff2Fonts(requests) { + var results = { + totalGain: 0, + fonts: [] + }; + + requests.forEach(function(req) { + if (!req.isWoff2 && req.weightCheck.sizeAsWoff2) { + var before = req.weightCheck.bodySize; + var after = req.weightCheck.sizeAsWoff2; + var gain = before - after; + + var type = null; + if (req.isWoff) { + type = 'woff'; + } else if (req.isTtf) { + type = 'ttf'; + } + + if (gain > 200) { + results.totalGain += gain; + + results.fonts.push({ + url: req.url, + originalSize: before, + type: type, + woff2Size: after, + gain: gain + }); + } + } + }); + + return results; + } + function listHeavyFonts(requests) { var list = []; var totalGain = 0; diff --git a/package.json b/package.json index 92ae9fb..1e31782 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,9 @@ "request": "2.88.0", "rimraf": "2.6.3", "temporary": "0.0.8", - "uglify-js": "3.4.9" + "ttf2woff2": "3.0.0", + "uglify-js": "3.4.9", + "woff-tools": "0.1.0" }, "devDependencies": { "chai": "~4.2.0", From 8e3b212df048bd8c923a824f75114b8861fb3214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 21 Dec 2020 01:27:34 +0100 Subject: [PATCH 33/59] Linting --- lib/tools/redownload/fontAnalyzer.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lib/tools/redownload/fontAnalyzer.js b/lib/tools/redownload/fontAnalyzer.js index 3cddcfc..13fa2ad 100644 --- a/lib/tools/redownload/fontAnalyzer.js +++ b/lib/tools/redownload/fontAnalyzer.js @@ -16,7 +16,10 @@ var FontAnalyzer = function() { return deferred.promise; } - var fileSize = entry.weightCheck.uncompressedSize; + var fileSize = entry.weightCheck.bodySize; + var ttf; + var woff2; + var newFileSize; if (entry.isWebFont) { debug('File %s is a font. Let\'s have a look inside!', entry.url); @@ -58,13 +61,11 @@ var FontAnalyzer = function() { try { - var fileSize = entry.weightCheck.bodySize; debug('Current file size is %d', fileSize); - var ttf = woffTools.toSfnt(entry.weightCheck.bodyBuffer); - var woff2 = ttf2woff2(ttf); - - var newFileSize = woff2.length; + ttf = woffTools.toSfnt(entry.weightCheck.bodyBuffer); + woff2 = ttf2woff2(ttf); + newFileSize = woff2.length; debug('New image size is %d', newFileSize); debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize)); @@ -82,12 +83,10 @@ var FontAnalyzer = function() { try { - var fileSize = entry.weightCheck.bodySize; debug('Current file size is %d', fileSize); - var woff2 = ttf2woff2(entry.weightCheck.bodyBuffer); - - var newFileSize = woff2.length; + woff2 = ttf2woff2(entry.weightCheck.bodyBuffer); + newFileSize = woff2.length; debug('New image size is %d', newFileSize); debug('Filesize is %d bytes smaller (-%d%)', fileSize - newFileSize, Math.round((fileSize - newFileSize) * 100 / fileSize)); From 65025b57c5c80100f475041cde6ddff3606dd933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 21 Dec 2020 01:28:01 +0100 Subject: [PATCH 34/59] Fix syntax error --- lib/metadata/scoreProfileGeneric.json | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 2ebd278..fe8fdce 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -88,7 +88,6 @@ "policies": { "fontsCount": 1, "heavyFonts": 0.5, - //"unusedUnicodeRanges": 0.5, "nonWoff2Fonts": 0.5 } }, From f25eb2901e37b5aec65867359f111fbcf77f2c1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 21 Dec 2020 01:32:43 +0100 Subject: [PATCH 35/59] Remove old phantomas modules --- .../custom_modules/core/scopeYLT/scopeYLT.js | 251 ------------------ .../modules/ajaxReqYLT/ajaxReqYLT.js | 30 --- .../modules/domHiddenYLT/domHiddenYLT.js | 86 ------ .../modules/eventYLT/eventYLT.js | 82 ------ .../modules/jsErrYLT/jsErrYLT.js | 49 ---- .../lazyLoadableYLT/lazyLoadableYLT.js | 81 ------ .../modules/requestsList/requestsList.js | 23 -- 7 files changed, 602 deletions(-) delete mode 100644 lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/ajaxReqYLT/ajaxReqYLT.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/domHiddenYLT/domHiddenYLT.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/eventYLT/eventYLT.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/jsErrYLT/jsErrYLT.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/lazyLoadableYLT/lazyLoadableYLT.js delete mode 100644 lib/tools/phantomas/custom_modules/modules/requestsList/requestsList.js diff --git a/lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js b/lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js deleted file mode 100644 index 322f504..0000000 --- a/lib/tools/phantomas/custom_modules/core/scopeYLT/scopeYLT.js +++ /dev/null @@ -1,251 +0,0 @@ -/** - * Overwritting the original spying functions in scope.js - * This is done so we now have a before AND an after callback on the spy - * - * @see http://code.jquery.com/jquery-1.10.2.js - * @see http://code.jquery.com/jquery-2.0.3.js - */ -/* global document: true, window: true */ - -exports.version = '0.2'; - -exports.module = function(phantomas) { - 'use strict'; - - var responseEndTime = Date.now(); - - phantomas.on('responseEnd', function() { - responseEndTime = Date.now(); - }); - - phantomas.on('init', function() { - phantomas.evaluate(function(responseEndTime) { - (function(phantomas) { - - // Overwritting phantomas spy function - (function() { - var enabled = true; - - // turn off spying to not include internal phantomas actions - function spyEnabled(state, reason) { - enabled = (state === true); - - phantomas.log('Spying ' + (enabled ? 'enabled' : 'disabled') + (reason ? ' - ' + reason : '')); - } - - phantomas.log('Overwritting phantomas spy function'); - - function spy(obj, fn, callbackBefore, callbackAfter) { - var origFn = obj && obj[fn]; - - if (typeof origFn !== 'function') { - return false; - } - - phantomas.log('Attaching a YLT spy to "' + fn + '" function...'); - - obj[fn] = function() { - var result; - var err; - - // Before - if (enabled && callbackBefore) { - callbackBefore.apply(this, arguments); - } - - // Execute - try { - - result = origFn.apply(this, arguments); - - } catch(e) { - - // Catching the err for the moment, because we need to make sure the callbackAfter function is called. - - phantomas.log('Error catched on spyed function "' + fn + '": ' + e); - phantomas.log(arguments); - - err = e; - - } finally { - - // After - if (enabled && callbackAfter) { - var args = Array.prototype.slice.call(arguments); - callbackAfter.apply(this, [result].concat(args)); - } - - if (err) { - phantomas.log('Re-throwing the error'); - throw err; - } - } - - return result; - }; - - // copy custom properties of original function to the mocked one - Object.keys(origFn).forEach(function(key) { - obj[fn][key] = origFn[key]; - }); - - obj[fn].prototype = origFn.prototype; - - return true; - } - - phantomas.spyEnabled = spyEnabled; - phantomas.spy = spy; - })(); - - - - // Adding some code for the Javascript execution tree construction - (function() { - - var root = new ContextTreeNode(null, {type: 'main'}); - var currentContext = root; - var depth = 0; - - - // Add a child but don't enter its context - function pushContext(data) { - - // Some data is not needed on subchildren - if (depth === 0) { - data.timestamp = Date.now() - responseEndTime; - data.loadingStep = phantomas.currentStep || ''; - } - - // Some data is not needed on subchildren - if (depth > 0) { - if (data.backtrace) { - delete data.backtrace; - } - if (data.resultsNumber) { - delete data.resultsNumber; - } - } - - currentContext.addChild(data); - } - - // Add a child to the current context and enter its context - function enterContext(data) { - - // Some data is not needed on subchildren - if (depth === 0) { - data.timestamp = Date.now() - responseEndTime; - data.loadingStep = phantomas.currentStep || ''; - } - - // Some data is not needed on subchildren - if (depth > 0) { - if (data.backtrace) { - delete data.backtrace; - } - if (data.resultsNumber) { - delete data.resultsNumber; - } - } - - currentContext = currentContext.addChild(data); - - depth ++; - } - - // Save given data in the current context and jump change current context to its parent - function leaveContext(moreData) { - - // Some data is not needed on subchildren - if (depth === 1) { - currentContext.data.time = Date.now() - currentContext.data.timestamp - responseEndTime; - } - - // Some data is not needed on subchildren - if (depth > 1) { - if (moreData && moreData.backtrace) { - delete moreData.backtrace; - } - if (moreData && moreData.resultsNumber) { - delete moreData.resultsNumber; - } - } - - // Merge previous data with moreData (ovewrites if exists) - if (moreData) { - for (var key in moreData) { - currentContext.data[key] = moreData[key]; - } - } - - var parent = currentContext.parent; - if (parent === null) { - console.error('Error: trying to close root context in ContextTree'); - } else { - currentContext = parent; - } - - depth --; - } - - function getContextData() { - return currentContext.data; - } - - // Returns a clean object, without the parent which causes recursive loops - function readFullTree() { - // Return null if the contextTree is not correctly closed - if (root !== currentContext) { - return null; - } - - function recusiveRead(node) { - if (node.children.length === 0) { - delete node.children; - } else { - for (var i=0, max=node.children.length ; i [%s]', url, method); - - if (async === false) { - phantomas.incrMetric('synchronousXHR'); - phantomas.addOffender('synchronousXHR', url); - phantomas.log('ajaxRequests: synchronous XMLHttpRequest call to <%s>', url); - } - }, true); - })(window.__phantomas); - }); - }); -}; \ No newline at end of file diff --git a/lib/tools/phantomas/custom_modules/modules/domHiddenYLT/domHiddenYLT.js b/lib/tools/phantomas/custom_modules/modules/domHiddenYLT/domHiddenYLT.js deleted file mode 100644 index a90e2bf..0000000 --- a/lib/tools/phantomas/custom_modules/modules/domHiddenYLT/domHiddenYLT.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Analyzes DOM hidden content - */ -/* global document: true, Node: true, window: true */ - -exports.version = '1.0.a'; - -exports.module = function(phantomas) { - 'use strict'; - - // total length of HTML of hidden elements (i.e. display: none) - phantomas.setMetric('hiddenContentSize'); // @desc the size of content of hidden elements on the page (with CSS display: none) @offenders - phantomas.setMetric('hiddenImages'); // @desc number of hidden images that can be lazy-loaded @offenders - - // HTML size - phantomas.on('report', function() { - phantomas.evaluate(function() { - (function(phantomas) { - var runner = new phantomas.nodeRunner(), - lazyLoadableImages = {}; - - phantomas.spyEnabled(false, 'analyzing hidden content'); - - runner.walk(document.body, function(node, depth) { - switch (node.nodeType) { - case Node.ELEMENT_NODE: - // @see https://developer.mozilla.org/en/DOM%3awindow.getComputedStyle - var styles = window.getComputedStyle(node); - - if (styles && styles.getPropertyValue('display') === 'none') { - if (typeof node.innerHTML === 'string') { - var size = node.innerHTML.length; - phantomas.incrMetric('hiddenContentSize', size); - - // log hidden containers bigger than 1 kB - if (size > 1024) { - phantomas.addOffender('hiddenContentSize', phantomas.getDOMPath(node) + ' (' + size + ' characters)'); - } - } - - // count hidden images that can be lazy loaded (issue #524) - var images = []; - if (node.tagName === 'IMG') { - images = [node]; - } else if (typeof node.querySelectorAll === 'function') { - images = node.querySelectorAll('img') || []; - } - - for (var i = 0, len = images.length; i < len; i++) { - var src = images[i].src, - path; - - if (src === '' || src.indexOf('data:image') === 0) continue; - - if (images[i].width === 1 && images[i].height === 1) continue; - - if (!lazyLoadableImages[src]) { - path = phantomas.getDOMPath(images[i]); - - lazyLoadableImages[src] = { - path: path - }; - } - } - - // don't run for child nodes as they're hidden as well - return false; - } - break; - } - }); - - Object.keys(lazyLoadableImages).forEach(function(img) { - var entry = lazyLoadableImages[img]; - - phantomas.incrMetric('hiddenImages'); - phantomas.addOffender('hiddenImages', img); - - phantomas.log('hiddenImages: <%s> image (%s) is hidden and can be lazy-loaded', img, entry.path); - }); - - phantomas.spyEnabled(true); - }(window.__phantomas)); - }); - }); -}; diff --git a/lib/tools/phantomas/custom_modules/modules/eventYLT/eventYLT.js b/lib/tools/phantomas/custom_modules/modules/eventYLT/eventYLT.js deleted file mode 100644 index fd95e74..0000000 --- a/lib/tools/phantomas/custom_modules/modules/eventYLT/eventYLT.js +++ /dev/null @@ -1,82 +0,0 @@ -/** - * Analyzes events bound to DOM elements - */ -/* global Document: true, Element: true, window: true */ - -exports.version = '0.4.a'; - -exports.module = function(phantomas) { - 'use strict'; - - phantomas.setMetric('eventsBound'); // @desc number of EventTarget.addEventListener calls - phantomas.setMetric('eventsDispatched'); // @desc number of EventTarget.dispatchEvent calls - phantomas.setMetric('eventsScrollBound'); // @desc number of scroll event bounds - - phantomas.on('init', function() { - phantomas.evaluate(function() { - (function(phantomas) { - // spy calls to EventTarget.addEventListener - // @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.addEventListener - function eventSpyBefore(eventType) { - /* jshint validthis: true */ - var path = phantomas.getDOMPath(this); - //phantomas.log('DOM event: "' + eventType + '" bound to "' + path + '"'); - - phantomas.incrMetric('eventsBound'); - phantomas.addOffender('eventsBound', '"%s" bound to "%s"', eventType, path); - phantomas.log('event ' + eventType + ' bound'); - - phantomas.enterContext({ - type: 'addEventListener', - callDetails: { - context: { - length: 1, - elements: [path] - }, - arguments: [eventType] - }, - backtrace: phantomas.getBacktrace() - }); - - // count window.addEventListener('scroll', ...) - issue #508 - if (eventType === 'scroll' && (path === 'window' || path === '#document')) { - phantomas.incrMetric('eventsScrollBound'); - phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', phantomas.getBacktrace(), path); - } - } - - function eventSpyAfter(result) { - phantomas.leaveContext(); - } - - phantomas.spy(Element.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter); - phantomas.spy(Document.prototype, 'addEventListener', eventSpyBefore, eventSpyAfter); - phantomas.spy(window, 'addEventListener', eventSpyBefore, eventSpyAfter); - - // spy calls to EventTarget.dispatchEvent - // @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget.dispatchEvent - phantomas.spy(Element.prototype, 'dispatchEvent', function(ev) { - /* jshint validthis: true */ - var path = phantomas.getDOMPath(this); - - phantomas.log('Core JS event: triggered "%s" on "%s"', ev.type, path); - - phantomas.incrMetric('eventsDispatched'); - phantomas.addOffender('eventsDispatched', '"%s" on "%s"', ev.type, path); - }); - })(window.__phantomas); - }); - }); - - phantomas.on('report', function() { - phantomas.evaluate(function() { - (function(phantomas) { - // Check if a window.onscroll function is defined - if (typeof(window.onscroll) === "function") { - phantomas.incrMetric('eventsScrollBound'); - phantomas.addOffender('eventsScrollBound', 'bound by %s on %s', '', 'window.onscroll'); - } - }(window.__phantomas)); - }); - }); -}; diff --git a/lib/tools/phantomas/custom_modules/modules/jsErrYLT/jsErrYLT.js b/lib/tools/phantomas/custom_modules/modules/jsErrYLT/jsErrYLT.js deleted file mode 100644 index e9975e2..0000000 --- a/lib/tools/phantomas/custom_modules/modules/jsErrYLT/jsErrYLT.js +++ /dev/null @@ -1,49 +0,0 @@ -/** - * Meters the number of page errors, and provides traces as offenders for "jsErrors" metric - */ - -exports.version = '0.3.a'; - -exports.module = function(phantomas) { - 'use strict'; - - phantomas.setMetric('jsErrors'); // @desc number of JavaScript errors - - function formatTrace(trace) { - var ret = []; - - if(Array.isArray(trace)) { - trace.forEach(function(entry) { - ret.push((entry.function ? entry.function + ' ' : '') + (entry.sourceURL || entry.file) + ':' + entry.line); - }); - } - - return ret; - } - - phantomas.on('jserror', function(msg, trace) { - trace = formatTrace(trace); - - phantomas.log(msg); - phantomas.log('Backtrace: ' + trace.join(' / ')); - - phantomas.incrMetric('jsErrors'); - phantomas.addOffender('jsErrors', msg + ' - ' + trace.join(' / ')); - - // Yeah, this is weird, i'm sending the error back to the browser... - phantomas.evaluate(function(msg, caller, trace) { - (function(phantomas) { - - phantomas.pushContext({ - type: 'error', - callDetails: { - arguments: [msg] - }, - caller: caller, - backtrace: trace - }); - - })(window.__phantomas); - }, msg, trace[0], trace.join(' / ')); - }); -}; diff --git a/lib/tools/phantomas/custom_modules/modules/lazyLoadableYLT/lazyLoadableYLT.js b/lib/tools/phantomas/custom_modules/modules/lazyLoadableYLT/lazyLoadableYLT.js deleted file mode 100644 index 8e2f042..0000000 --- a/lib/tools/phantomas/custom_modules/modules/lazyLoadableYLT/lazyLoadableYLT.js +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Analyzes images and detects which one can be lazy-loaded (are below the fold) - * - * @see https://github.com/macbre/phantomas/issues/494 - */ -/* global document: true, window: true */ - -exports.version = '1.0.a'; - -exports.module = function(phantomas) { - 'use strict'; - - phantomas.setMetric('lazyLoadableImagesBelowTheFold'); // @desc number of images displayed below the fold that can be lazy-loaded - - phantomas.on('report', function() { - phantomas.log('lazyLoadableImages: analyzing which images can be lazy-loaded...'); - - phantomas.evaluate(function() { - (function(phantomas) { - phantomas.spyEnabled(false, 'analyzing which images can be lazy-loaded'); - - var images = document.body.getElementsByTagName('img'), - i, - len = images.length, - offset, - path, - processedImages = {}, - src, - viewportHeight = window.innerHeight, - // Add an offset of 100px under the height of the screen - LAZYLOAD_OFFSET = 100; - - phantomas.log('lazyLoadableImages: %d image(s) found, assuming %dpx offset to be the fold', len, viewportHeight); - - for (i = 0; i < len; i++) { - // @see https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect - offset = images[i].getBoundingClientRect().top; - src = images[i].src; - - // ignore base64-encoded images - if (src === '' || /^data:/.test(src)) { - continue; - } - - path = phantomas.getDOMPath(images[i]); - - // get the most top position for a given image (deduplicate by src) - if (typeof processedImages[src] === 'undefined') { - processedImages[src] = { - offset: offset, - path: path - }; - } - - // maybe there's the same image loaded above the fold? - if (offset < processedImages[src].offset) { - processedImages[src] = { - offset: offset, - path: path - }; - } - } - - phantomas.log('lazyLoadableImages: checking %d unique image(s)', Object.keys(processedImages).length); - - Object.keys(processedImages).forEach(function(src) { - var img = processedImages[src]; - - if (img.offset > viewportHeight + LAZYLOAD_OFFSET) { - phantomas.log('lazyLoadableImages: <%s> image (%s) is below the fold (at %dpx)', src, img.path, img.offset); - - phantomas.incrMetric('lazyLoadableImagesBelowTheFold'); - phantomas.addOffender('lazyLoadableImagesBelowTheFold', src); - } - }); - - phantomas.spyEnabled(true); - })(window.__phantomas); - }); - }); -}; \ No newline at end of file diff --git a/lib/tools/phantomas/custom_modules/modules/requestsList/requestsList.js b/lib/tools/phantomas/custom_modules/modules/requestsList/requestsList.js deleted file mode 100644 index a36bfe1..0000000 --- a/lib/tools/phantomas/custom_modules/modules/requestsList/requestsList.js +++ /dev/null @@ -1,23 +0,0 @@ -/** - * Retries download on every request to get the real file size - * - */ - -exports.version = '0.1'; - -exports.module = function(phantomas) { - 'use strict'; - - phantomas.setMetric('requestsList'); - - var requests = []; - - phantomas.on('recv', function(entry, res) { - requests.push(entry); - }); - - phantomas.on('report', function() { - phantomas.setMetric('requestsList', true, true); - phantomas.addOffender('requestsList', JSON.stringify(requests)); - }); -}; From c4b7bd4d7146fa90f7b5c0d741bb609c6440c9dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 21 Dec 2020 02:09:24 +0100 Subject: [PATCH 36/59] Fix error --- lib/tools/redownload/fontAnalyzer.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/tools/redownload/fontAnalyzer.js b/lib/tools/redownload/fontAnalyzer.js index 13fa2ad..a45874e 100644 --- a/lib/tools/redownload/fontAnalyzer.js +++ b/lib/tools/redownload/fontAnalyzer.js @@ -16,11 +16,6 @@ var FontAnalyzer = function() { return deferred.promise; } - var fileSize = entry.weightCheck.bodySize; - var ttf; - var woff2; - var newFileSize; - if (entry.isWebFont) { debug('File %s is a font. Let\'s have a look inside!', entry.url); @@ -50,6 +45,11 @@ var FontAnalyzer = function() { debug('Entering font format converter...'); + var fileSize = entry.weightCheck.bodySize; + var ttf; + var woff2; + var newFileSize; + if (entry.isWoff2) { debug('File is already a woff2.'); From be5549bf812c3d5f0945accc97433f43012e0e6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 21 Dec 2020 02:14:30 +0100 Subject: [PATCH 37/59] Replace the HTML minifier so that it minifies inline JS and CSS --- lib/tools/redownload/fileMinifier.js | 34 ++++++++++++++++------------ package.json | 2 +- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/tools/redownload/fileMinifier.js b/lib/tools/redownload/fileMinifier.js index d7f86a3..5e4f97e 100644 --- a/lib/tools/redownload/fileMinifier.js +++ b/lib/tools/redownload/fileMinifier.js @@ -3,7 +3,7 @@ var debug = require('debug')('ylt:fileMinifier'); var Q = require('q'); var UglifyJS = require('uglify-js'); var CleanCSS = require('clean-css'); -var Minimize = require('minimize'); +var htmlMinifier = require('html-minifier'); var FileMinifier = function() { @@ -175,19 +175,25 @@ var FileMinifier = function() { function minifyHtml(body) { var deferred = Q.defer(); - var minimize = new Minimize({ - empty: true, // KEEP empty attributes - conditionals: true, // KEEP conditional internet explorer comments - spare: true // KEEP redundant attributes - }); - - minimize.parse(body, function (error, data) { - if (error) { - deferred.reject(error); - } else { - deferred.resolve(data); - } - }); + try { + var result = htmlMinifier.minify(body, { + collapseWhitespace: true, + conservativeCollapse: true, + continueOnParseError: true, + decodeEntities: true, + minifyCSS: true, + minifyJS: true, + preserveLineBreaks: true, + removeAttributeQuotes: true, + removeComments: true, + removeScriptTypeAttributes: true, + removeStyleLinkTypeAttributes: true + }); + deferred.resolve(result); + } catch(err) { + deferred.reject(err); + } + return deferred.promise; } diff --git a/package.json b/package.json index 1e31782..3a2e0e7 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "ejs": "2.6.1", "express": "4.16.4", "fontkit": "1.7.8", + "html-minifier": "4.0.0", "image-size": "0.7.1", "imagemin": "6.1.0", "imagemin-jpegoptim": "6.0.0", @@ -59,7 +60,6 @@ "jimp": "0.6.0", "md5": "2.2.1", "meow": "5.0.0", - "minimize": "2.2.0", "parse-color": "1.0.0", "phantomas": "github:macbre/phantomas#devel", "ps-node": "0.1.6", From 51dc1b16180bc2e4e49964c70174ca3e1c7c3b1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 21 Dec 2020 10:21:20 +0100 Subject: [PATCH 38/59] Add Brotli compression as the default compresion --- front/src/views/rule.html | 36 ++++--- lib/metadata/policies.js | 14 +-- lib/metadata/scoreProfileGeneric.json | 4 +- lib/tools/redownload/brotliCompressor.js | 115 +++++++++++++++++++++ lib/tools/redownload/contentTypeChecker.js | 11 +- lib/tools/redownload/fontAnalyzer.js | 2 +- lib/tools/redownload/gzipCompressor.js | 15 +-- lib/tools/redownload/redownload.js | 65 ++++++++---- package.json | 1 + test/core/gzipCompressorTest.js | 64 ++++++------ 10 files changed, 234 insertions(+), 93 deletions(-) create mode 100644 lib/tools/redownload/brotliCompressor.js diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 9b063a6..19e653b 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -311,32 +311,38 @@
-

Current weight: {{image.original | bytes}}

-

Current weight: {{image.original | bytes}} ({{image.afterCompression | bytes}} gzipped)

-

With a lossless optimization:
{{image.afterOptimizationAndCompression | bytes}} gzipped (-{{image.gain | bytes}} gzipped)

-

With a lossless optimization:
{{image.lossless | bytes}} (-{{image.gain | bytes}})

-

With a lossy optimization:
{{image.afterOptimizationAndCompression | bytes}} gzipped (-{{image.gain | bytes}} gzipped)

-

With a lossy optimization:
{{image.lossy | bytes}} (-{{image.gain | bytes}})

+

Current weight: {{image.original | bytes}}

+

Current weight: {{image.original | bytes}} ({{image.afterGzipCompression | bytes}} gzipped)

+

With a lossless optimization:
{{image.afterOptimizationAndGzipCompression | bytes}} gzipped (-{{image.gain | bytes}} gzipped)

+

With a lossless optimization:
{{image.lossless | bytes}} (-{{image.gain | bytes}})

+

With a lossy optimization:
{{image.afterOptimizationAndGzipCompression | bytes}} gzipped (-{{image.gain | bytes}} gzipped)

+

With a lossy optimization:
{{image.lossy | bytes}} (-{{image.gain | bytes}})

-
+

{{rule.value | bytes}} could be saved on

File
Current weight
-
Gzipped
+
Gzip
+
Brotli
Gain
- +
-
{{file.original | bytes}}
-
{{file.gzipped | bytes}}
+
{{file.originalSize | bytes}}
+ +
already gzipped
+
{{file.gzipped | bytes}}
+ +
{{file.brotlified | bytes}}
+
-{{file.gain | bytes}}
@@ -355,10 +361,10 @@
-
{{file.original | bytes}} (gzipped)
-
{{file.original | bytes}} ({{file.afterCompression | bytes}} gzipped)
-
{{file.afterOptimizationAndCompression | bytes}} (gzipped)
-
{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} gzipped)
+
{{file.original | bytes}} (gzipped)
+
{{file.original | bytes}} ({{file.afterGzipCompression | bytes}} gzipped)
+
{{file.afterOptimizationAndGzipCompression | bytes}} (gzipped)
+
{{file.optimized | bytes}} ({{file.afterOptimizationAndGzipCompression | bytes}} gzipped)
-{{file.gain | bytes}}
diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index b6d7e3d..4ab47be 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -786,13 +786,13 @@ var policies = { "isAbnormalThreshold": 10, "hasOffenders": true }, - "gzipCompression": { + "compression": { "tool": "redownload", - "label": "Gzip compression", - "message": "

Measures the number of bytes that could be saved by compressing file transfers.

Gzip is a powerfull weight reducer and should be enabled on text-based assets in your server's configuration. Note that gzipping small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. Here is a list of Content-Types that should be gzipped.

", - "isOkThreshold": 5125, - "isBadThreshold": 81920, - "isAbnormalThreshold": 153600, + "label": "Gzip/Brotli compression", + "message": "

Measures the number of bytes that could be saved by compressing textual files.

Gzip is a good old weight reducer. But Brotli is a new-generation compression tool and it gives better results. All major server systems are now compatible with Brotli.

Note that compressing small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. Here is a list of Content-Types that should be compressed.

", + "isOkThreshold": 20480, + "isBadThreshold": 204800, + "isAbnormalThreshold": 409600, "hasOffenders": true, "unit": 'bytes' }, @@ -932,7 +932,7 @@ var policies = { "label": "Woff 2", "message": "

The fonts listed here could be lighter if they were served with the latest woff 2 font file format. Some online tools can help you easily convert older formats to woff 2.

", "isOkThreshold": 0, - "isBadThreshold": 61440, + "isBadThreshold": 51200, "isAbnormalThreshold": 122880, "hasOffenders": true, "unit": 'bytes' diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index fe8fdce..3131fef 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -5,8 +5,8 @@ "policies": { "totalWeight": 5, "imageOptimization": 2, - "imagesTooLarge": 2, - "gzipCompression": 2, + "imagesTooLarge": 1, + "compression": 2, "fileMinification": 1 } }, diff --git a/lib/tools/redownload/brotliCompressor.js b/lib/tools/redownload/brotliCompressor.js new file mode 100644 index 0000000..cfe5913 --- /dev/null +++ b/lib/tools/redownload/brotliCompressor.js @@ -0,0 +1,115 @@ +var debug = require('debug')('ylt:brotliCompressor'); + +var Q = require('q'); +var brotli = require('brotli'); + +var gzipCompressor = require('./gzipCompressor'); + + +var GzipCompressor = function() { + + var BROTLI_COMPRESSION_LEVEL = 9; + + function compressFile(entry) { + debug('Entering brotli compressor'); + return brotlifyFile(entry) + + .then(brotliOptimizedFile); + } + + // Compress with Brotli files that were not compressed or not with brotli (with gzip or deflate) + function brotlifyFile(entry) { + var deferred = Q.defer(); + + if (gzipCompressor.entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.bodyBuffer) { + + if (!entry.weightCheck.isCompressed) { + debug('File %s was not compressed at all, trying Brotli over it.', entry.url); + } else if (entry.weightCheck.compressionTool !== 'brotli') { + debug('File %s was compressed with %s. Trying with Brotli.', entry.url, entry.weightCheck.compressionTool); + } + + try { + + var buffer = brotli.compress(entry.weightCheck.bodyBuffer, { + mode: 0, + quality: 9 + }); + + if (buffer) { + var compressedSize = buffer.length; + + if (!entry.weightCheck.isCompressed) { + debug('Brotli size is %d, was %d, this is %d% better.', compressedSize, entry.weightCheck.bodySize, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize)); + } else if (entry.weightCheck.compressionTool !== 'brotli') { + debug('Brotli size is %d, was %d with %s, this is %d% better.', compressedSize, entry.weightCheck.bodySize, entry.weightCheck.compressionTool, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize)); + } + + entry.weightCheck.afterBrotliCompression = compressedSize; + + } else { + debug('Failed to brotlify %s', entry.url); + } + + deferred.resolve(entry); + + } catch (err) { + debug(err); + deferred.reject(err); + } + } else { + debug('Compression not needed'); + deferred.resolve(entry); + } + + return deferred.promise; + } + + // Gzip a file after minification or optimization if this step was successful + function brotliOptimizedFile(entry) { + var deferred = Q.defer(); + + if (gzipCompressor.entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) { + debug('Trying to brotlify file after minification: %s', entry.url); + + var uncompressedSize = entry.weightCheck.optimized; + + try { + + var buffer = brotli.compress(entry.weightCheck.bodyAfterOptimization, { + mode: 1, + quality: BROTLI_COMPRESSION_LEVEL + }); + + if (buffer) { + var compressedSize = buffer.length; + + debug('Correctly brotlified the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize); + + entry.weightCheck.afterOptimizationAndBrotliCompression = compressedSize; + + } else { + debug('Failed to brotlify %s', entry.url); + } + + deferred.resolve(entry); + + } catch (err) { + debug(err); + deferred.reject(err); + } + + } else { + debug('Compressing optimized file not needed'); + deferred.resolve(entry); + } + + return deferred.promise; + } + + return { + compressFile: compressFile + }; +}; + +module.exports = new GzipCompressor(); \ No newline at end of file diff --git a/lib/tools/redownload/contentTypeChecker.js b/lib/tools/redownload/contentTypeChecker.js index e83924f..58fb3df 100644 --- a/lib/tools/redownload/contentTypeChecker.js +++ b/lib/tools/redownload/contentTypeChecker.js @@ -56,15 +56,12 @@ var ContentTypeChecker = function() { try { foundType = findContentType(entry.weightCheck.bodyBuffer); - if (foundType !== null) { - if (foundType.type === 'webfont') { - // Always rewrite fonts for woff2 checking - rewriteContentType(entry, foundType); - } else if (foundType.type !== entry.type) { - // For other kind of files, just rewrite if needed + // If it's an image or a font, then rewrite. + if (foundType !== null && (foundType.type === 'image' || foundType.type === 'webfont')) { + if (foundType.type !== entry.type) { debug('Content type %s is wrong for %s. It should be %s.', entry.type, entry.ulr, foundType.type); - rewriteContentType(entry, foundType); } + rewriteContentType(entry, foundType); } } catch(err) { diff --git a/lib/tools/redownload/fontAnalyzer.js b/lib/tools/redownload/fontAnalyzer.js index a45874e..a5bad4e 100644 --- a/lib/tools/redownload/fontAnalyzer.js +++ b/lib/tools/redownload/fontAnalyzer.js @@ -125,7 +125,7 @@ var FontAnalyzer = function() { name: font.fullName || font.postscriptName || font.familyName, numGlyphs: font.numGlyphs, averageGlyphComplexity: getAverageGlyphComplexity(font), - compressedWeight: entry.weightCheck.afterCompression || entry.weightCheck.bodySize, + compressedWeight: entry.weightCheck.afterGzipCompression || entry.weightCheck.bodySize, unicodeRanges: readUnicodeRanges(font.characterSet, charsListOnPage), numGlyphsInCommonWithPageContent: countPossiblyUsedGlyphs(getCharacterSetAsString(font.characterSet), charsListOnPage) }; diff --git a/lib/tools/redownload/gzipCompressor.js b/lib/tools/redownload/gzipCompressor.js index 56351ab..bf379e4 100644 --- a/lib/tools/redownload/gzipCompressor.js +++ b/lib/tools/redownload/gzipCompressor.js @@ -6,7 +6,7 @@ var zlib = require('zlib'); var GzipCompressor = function() { function compressFile(entry) { - debug('Entering compressFile'); + debug('Entering gzip compressor'); return gzipUncompressedFile(entry) .then(gzipOptimizedFile); @@ -16,7 +16,7 @@ var GzipCompressor = function() { function gzipUncompressedFile(entry) { var deferred = Q.defer(); - if (entryTypeCanBeGzipped(entry) && entry.weightCheck && !entry.weightCheck.isCompressed && entry.weightCheck.bodyBuffer) { + if (entryTypeCanBeCompressed(entry) && entry.weightCheck && !entry.weightCheck.isCompressed && entry.weightCheck.bodyBuffer) { debug('Compression missing, trying to gzip file %s', entry.url); var uncompressedSize = entry.weightCheck.uncompressedSize; @@ -33,7 +33,7 @@ var GzipCompressor = function() { if (gainIsEnough(uncompressedSize, compressedSize)) { debug('File correctly gziped, was %d and is now %d bytes', uncompressedSize, compressedSize); - entry.weightCheck.afterCompression = compressedSize; + entry.weightCheck.afterGzipCompression = compressedSize; } else { debug('Gzip gain is not enough, was %d and is now %d bytes', uncompressedSize, compressedSize); } @@ -53,7 +53,7 @@ var GzipCompressor = function() { function gzipOptimizedFile(entry) { var deferred = Q.defer(); - if (entryTypeCanBeGzipped(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) { + if (entryTypeCanBeCompressed(entry) && entry.weightCheck && entry.weightCheck.isOptimized === false) { debug('Trying to gzip file after minification: %s', entry.url); var uncompressedSize = entry.weightCheck.optimized; @@ -68,7 +68,7 @@ var GzipCompressor = function() { var compressedSize = buffer.length; debug('Correctly gziped the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize); - entry.weightCheck.afterOptimizationAndCompression = compressedSize; + entry.weightCheck.afterOptimizationAndGzipCompression = compressedSize; deferred.resolve(entry); } @@ -89,13 +89,14 @@ var GzipCompressor = function() { return (gain > 2048 || (ratio > 0.2 && gain > 100)); } - function entryTypeCanBeGzipped(entry) { + function entryTypeCanBeCompressed(entry) { return entry.isJS || entry.isCSS || entry.isHTML || entry.isJSON || entry.isSVG || entry.isTTF || entry.isXML || entry.isFavicon; } return { compressFile: compressFile, - entryTypeCanBeGzipped: entryTypeCanBeGzipped + gainIsEnough: gainIsEnough, + entryTypeCanBeCompressed: entryTypeCanBeCompressed }; }; diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index 86927c4..89ceff9 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -17,6 +17,7 @@ var md5 = require('md5'); var imageOptimizer = require('./imageOptimizer'); var fileMinifier = require('./fileMinifier'); var gzipCompressor = require('./gzipCompressor'); +var brotliCompressor = require('./brotliCompressor'); var contentTypeChecker = require('./contentTypeChecker'); var fontAnalyzer = require('./fontAnalyzer'); var imageDimensions = require('./imageDimensions'); @@ -80,6 +81,8 @@ var Redownload = function() { .then(gzipCompressor.compressFile) + .then(brotliCompressor.compressFile) + .then(function(entry) { return fontAnalyzer.analyzeFont(entry, differentCharacters); }) @@ -151,9 +154,9 @@ var Redownload = function() { offenders.fileMinification = listFilesNotMinified(results); metrics.fileMinification = offenders.fileMinification.totalGain; - // Gzip compression - offenders.gzipCompression = listFilesNotGzipped(results); - metrics.gzipCompression = offenders.gzipCompression.totalGain; + // Gzip/Brotli compression + offenders.compression = listFilesNotBrotlified(results); + metrics.compression = offenders.compression.totalGain; // Small requests offenders.smallRequests = listSmallRequests(results); @@ -338,8 +341,8 @@ var Redownload = function() { requests.forEach(function(req) { if (req.weightCheck.bodySize > 0 && imageOptimizer.entryTypeCanBeOptimized(req) && req.weightCheck.isOptimized === false) { - var before = req.weightCheck.afterCompression || req.weightCheck.bodySize; - var after = req.weightCheck.afterOptimizationAndCompression || req.weightCheck.optimized; + var before = req.weightCheck.afterGzipCompression || req.weightCheck.bodySize; + var after = req.weightCheck.afterOptimizationAndGzipCompression || req.weightCheck.optimized; var gain = before - after; if (gain > 200) { @@ -349,8 +352,8 @@ var Redownload = function() { url: req.url, original: req.weightCheck.bodySize, isCompressed: req.weightCheck.isCompressed, - afterCompression: req.weightCheck.afterCompression, - afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression, + afterGzipCompression: req.weightCheck.afterGzipCompression, + afterOptimizationAndGzipCompression: req.weightCheck.afterOptimizationAndGzipCompression, lossless: req.weightCheck.lossless, lossy: req.weightCheck.lossy, gain: gain @@ -398,8 +401,8 @@ var Redownload = function() { requests.forEach(function(req) { if (req.weightCheck.bodySize > 0 && fileMinifier.entryTypeCanBeMinified(req) && req.weightCheck.isOptimized === false) { - var before = req.weightCheck.afterCompression || req.weightCheck.bodySize; - var after = req.weightCheck.afterOptimizationAndCompression || req.weightCheck.optimized; + var before = req.weightCheck.afterGzipCompression || req.weightCheck.bodySize; + var after = req.weightCheck.afterOptimizationAndGzipCompression || req.weightCheck.optimized; var gain = before - after; if (gain > 200) { @@ -409,8 +412,8 @@ var Redownload = function() { url: req.url, original: req.weightCheck.bodySize, isCompressed: req.weightCheck.isCompressed, - afterCompression: req.weightCheck.afterCompression, - afterOptimizationAndCompression: req.weightCheck.afterOptimizationAndCompression, + afterGzipCompression: req.weightCheck.afterGzipCompression, + afterOptimizationAndGzipCompression: req.weightCheck.afterOptimizationAndGzipCompression, optimized: req.weightCheck.optimized, gain: gain }); @@ -421,24 +424,37 @@ var Redownload = function() { return results; } - function listFilesNotGzipped(requests) { + function listFilesNotBrotlified(requests) { var results = { totalGain: 0, files: [] }; requests.forEach(function(req) { - if (req.weightCheck.uncompressedSize && req.weightCheck.isCompressed === false && req.weightCheck.afterCompression) { - var gain = req.weightCheck.uncompressedSize - req.weightCheck.afterCompression; - - results.totalGain += gain; - - results.files.push({ + if (req.weightCheck.compressionTool !== 'brotli') { + + var file = { url: req.url, - original: req.weightCheck.uncompressedSize, - gzipped: req.weightCheck.afterCompression, - gain: gain - }); + wasCompressed: req.weightCheck.isCompressed, + brotlified: req.weightCheck.afterBrotliCompression + }; + + if (req.weightCheck.isCompressed) { + // The file was already gzipped (or deflated) + file.originalSize = req.weightCheck.bodySize; + file.gain = req.weightCheck.bodySize - req.weightCheck.afterBrotliCompression; + } else { + // The file was not compressed at all + file.originalSize = req.weightCheck.uncompressedSize; + file.gzipped = req.weightCheck.afterGzipCompression; + file.gain = req.weightCheck.uncompressedSize - req.weightCheck.afterBrotliCompression; + } + + // Just checking a last time if the gain is positive + if (file.gain > 200) { + results.totalGain += file.gain; + results.files.push(file); + } } }); @@ -817,6 +833,7 @@ var Redownload = function() { var bodySize = 0; // bytes size over the wire var bodyChunks = []; // an array of buffers var isCompressed = false; + var compressionTool = ''; function tally() { @@ -832,6 +849,7 @@ var Redownload = function() { headersSize: Buffer.byteLength(rawHeaders, 'utf8'), bodySize: bodySize, isCompressed: isCompressed, + compressionTool: compressionTool, uncompressedSize: uncompressedSize }; @@ -849,6 +867,7 @@ var Redownload = function() { uncompressedSize += data.length; }).on('end', function () { isCompressed = true; + compressionTool = 'gzip'; tally(); }).on('error', function(err) { debug('Error while decoding %s', requestOptions.url); @@ -871,6 +890,7 @@ var Redownload = function() { uncompressedSize += data.length; }).on('end', function () { isCompressed = true; + compressionTool = 'deflate'; tally(); }).on('error', function(err) { debug('Error while decoding %s', requestOptions.url); @@ -893,6 +913,7 @@ var Redownload = function() { uncompressedSize += data.length; }).on('end', function () { isCompressed = true; + compressionTool = 'brotli'; tally(); }).on('error', function(err) { debug('Error while decoding %s', requestOptions.url); diff --git a/package.json b/package.json index 3a2e0e7..c3efdf8 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "angular-sanitize": "1.7.7", "async": "2.6.1", "body-parser": "1.18.3", + "brotli": "1.3.2", "chart.js": "2.7.3", "clean-css": "4.2.1", "color-diff": "1.1.0", diff --git a/test/core/gzipCompressorTest.js b/test/core/gzipCompressorTest.js index 4439f1c..2ec0b93 100644 --- a/test/core/gzipCompressorTest.js +++ b/test/core/gzipCompressorTest.js @@ -36,8 +36,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -79,9 +79,9 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); - newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.not.undefined; - newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(newEntry.weightCheck.afterCompression); + newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize); + newEntry.weightCheck.should.have.a.property('afterOptimizationAndGzipCompression').that.is.not.undefined; + newEntry.weightCheck.should.have.a.property('afterOptimizationAndGzipCompression').that.is.below(newEntry.weightCheck.afterGzipCompression); done(); }) @@ -123,9 +123,9 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.not.undefined; - newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(gzipedSize); - newEntry.weightCheck.should.have.a.property('afterOptimizationAndCompression').that.is.below(minifiedSize); + newEntry.weightCheck.should.have.a.property('afterOptimizationAndGzipCompression').that.is.not.undefined; + newEntry.weightCheck.should.have.a.property('afterOptimizationAndGzipCompression').that.is.below(gzipedSize); + newEntry.weightCheck.should.have.a.property('afterOptimizationAndGzipCompression').that.is.below(minifiedSize); done(); }) @@ -165,8 +165,8 @@ describe('gzipCompressor', function() { .then(function(newEntry) { newEntry.weightCheck.should.not.have.a.property('minified'); newEntry.weightCheck.should.not.have.a.property('bodyAfterOptimization'); - newEntry.weightCheck.should.not.have.a.property('afterCompression'); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.not.have.a.property('afterGzipCompression'); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -201,8 +201,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -237,8 +237,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -274,8 +274,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -310,8 +310,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -346,8 +346,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -383,8 +383,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -420,8 +420,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.have.a.property('afterCompression').that.is.below(fileSize); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.have.a.property('afterGzipCompression').that.is.below(fileSize); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -457,8 +457,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.not.have.a.property('afterCompression'); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.not.have.a.property('afterGzipCompression'); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -495,8 +495,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.not.have.a.property('afterCompression'); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.not.have.a.property('afterGzipCompression'); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -532,8 +532,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.not.have.a.property('afterCompression'); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.not.have.a.property('afterGzipCompression'); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) @@ -569,8 +569,8 @@ describe('gzipCompressor', function() { gzipCompressor.compressFile(entry) .then(function(newEntry) { - newEntry.weightCheck.should.not.have.a.property('afterCompression'); - newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndCompression'); + newEntry.weightCheck.should.not.have.a.property('afterGzipCompression'); + newEntry.weightCheck.should.not.have.a.property('afterOptimizationAndGzipCompression'); done(); }) From 6b58b72028529d22be3ae40d4a50ae8dd98f25fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Mon, 21 Dec 2020 23:59:39 +0100 Subject: [PATCH 39/59] Replace brotli module by zlib --- lib/tools/redownload/brotliCompressor.js | 88 +++++++++++------------- package.json | 1 - 2 files changed, 40 insertions(+), 49 deletions(-) diff --git a/lib/tools/redownload/brotliCompressor.js b/lib/tools/redownload/brotliCompressor.js index cfe5913..8a1c7dd 100644 --- a/lib/tools/redownload/brotliCompressor.js +++ b/lib/tools/redownload/brotliCompressor.js @@ -1,7 +1,7 @@ -var debug = require('debug')('ylt:brotliCompressor'); +var debug = require('debug')('ylt:brotliCompressor'); -var Q = require('q'); -var brotli = require('brotli'); +var Q = require('q'); +var zlib = require('zlib'); var gzipCompressor = require('./gzipCompressor'); @@ -29,34 +29,31 @@ var GzipCompressor = function() { debug('File %s was compressed with %s. Trying with Brotli.', entry.url, entry.weightCheck.compressionTool); } - try { - - var buffer = brotli.compress(entry.weightCheck.bodyBuffer, { - mode: 0, - quality: 9 - }); - - if (buffer) { - var compressedSize = buffer.length; - - if (!entry.weightCheck.isCompressed) { - debug('Brotli size is %d, was %d, this is %d% better.', compressedSize, entry.weightCheck.bodySize, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize)); - } else if (entry.weightCheck.compressionTool !== 'brotli') { - debug('Brotli size is %d, was %d with %s, this is %d% better.', compressedSize, entry.weightCheck.bodySize, entry.weightCheck.compressionTool, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize)); + zlib.brotliCompress(entry.weightCheck.bodyBuffer, { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: 9 } + }, function(err, buffer) { + if (err) { + debug('Could not compress uncompressed file with brotli'); + debug(err); - entry.weightCheck.afterBrotliCompression = compressedSize; + deferred.reject(err); + } else { + var compressedSize = buffer.length; - } else { - debug('Failed to brotlify %s', entry.url); + if (!entry.weightCheck.isCompressed) { + debug('Brotli size is %d, was %d, this is %d% better.', compressedSize, entry.weightCheck.bodySize, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize)); + } else if (entry.weightCheck.compressionTool !== 'brotli') { + debug('Brotli size is %d, was %d with %s, this is %d% better.', compressedSize, entry.weightCheck.bodySize, entry.weightCheck.compressionTool, Math.round((entry.weightCheck.bodySize - compressedSize) * 100 / entry.weightCheck.bodySize)); + } + + entry.weightCheck.afterBrotliCompression = compressedSize; + + deferred.resolve(entry); + } } - - deferred.resolve(entry); - - } catch (err) { - debug(err); - deferred.reject(err); - } + ); } else { debug('Compression not needed'); deferred.resolve(entry); @@ -74,31 +71,26 @@ var GzipCompressor = function() { var uncompressedSize = entry.weightCheck.optimized; - try { + zlib.brotliCompress(Buffer.from(entry.weightCheck.bodyAfterOptimization, 'utf8'), { + params: { + [zlib.constants.BROTLI_PARAM_QUALITY]: 9 + } + }, function(err, buffer) { + if (err) { + debug('Could not compress minified file with brotli'); + debug(err); - var buffer = brotli.compress(entry.weightCheck.bodyAfterOptimization, { - mode: 1, - quality: BROTLI_COMPRESSION_LEVEL - }); + deferred.reject(err); + } else { + var compressedSize = buffer.length; - if (buffer) { - var compressedSize = buffer.length; + debug('Correctly brotlified the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize); + entry.weightCheck.afterOptimizationAndBrotliCompression = compressedSize; - debug('Correctly brotlified the minified file, was %d and is now %d bytes', uncompressedSize, compressedSize); - - entry.weightCheck.afterOptimizationAndBrotliCompression = compressedSize; - - } else { - debug('Failed to brotlify %s', entry.url); + deferred.resolve(entry); + } } - - deferred.resolve(entry); - - } catch (err) { - debug(err); - deferred.reject(err); - } - + ); } else { debug('Compressing optimized file not needed'); deferred.resolve(entry); diff --git a/package.json b/package.json index c3efdf8..3a2e0e7 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ "angular-sanitize": "1.7.7", "async": "2.6.1", "body-parser": "1.18.3", - "brotli": "1.3.2", "chart.js": "2.7.3", "clean-css": "4.2.1", "color-diff": "1.1.0", From 8f06ceaeddd87f9608dbfdb26117d5197d622a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 22 Dec 2020 00:00:44 +0100 Subject: [PATCH 40/59] Compare minification with brotli instead of gzip --- front/src/views/rule.html | 24 +++++----- lib/metadata/policies.js | 6 +-- lib/tools/redownload/redownload.js | 71 ++++++++++++++++++++++++++---- 3 files changed, 78 insertions(+), 23 deletions(-) diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 19e653b..d2c8c77 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -311,12 +311,14 @@
-

Current weight: {{image.original | bytes}}

-

Current weight: {{image.original | bytes}} ({{image.afterGzipCompression | bytes}} gzipped)

-

With a lossless optimization:
{{image.afterOptimizationAndGzipCompression | bytes}} gzipped (-{{image.gain | bytes}} gzipped)

-

With a lossless optimization:
{{image.lossless | bytes}} (-{{image.gain | bytes}})

-

With a lossy optimization:
{{image.afterOptimizationAndGzipCompression | bytes}} gzipped (-{{image.gain | bytes}} gzipped)

-

With a lossy optimization:
{{image.lossy | bytes}} (-{{image.gain | bytes}})

+

Current weight: {{image.originalWeigth | bytes}}

+

Current weight: {{image.originalWeigth | bytes}} ({{image.originalCompressedWeight | bytes}} compressed)

+ +

With a lossless optimization:
{{image.afterOptimizationAndCompression | bytes}} compressed (-{{image.gain | bytes}} compressed)

+

With a lossless optimization:
{{image.lossless | bytes}} (-{{image.gain | bytes}})

+ +

With a lossy optimization:
{{image.afterOptimizationAndCompression | bytes}} compressed (-{{image.gain | bytes}} compressed)

+

With a lossy optimization:
{{image.lossy | bytes}} (-{{image.gain | bytes}})

@@ -359,12 +361,12 @@
- +
-
{{file.original | bytes}} (gzipped)
-
{{file.original | bytes}} ({{file.afterGzipCompression | bytes}} gzipped)
-
{{file.afterOptimizationAndGzipCompression | bytes}} (gzipped)
-
{{file.optimized | bytes}} ({{file.afterOptimizationAndGzipCompression | bytes}} gzipped)
+
{{file.originalWeigth | bytes}} (compressed)
+
{{file.originalWeigth | bytes}} ({{file.originalCompressedWeight | bytes}} compressed)
+
{{file.afterOptimizationAndCompression | bytes}} (compressed)
+
{{file.optimized | bytes}} ({{file.afterOptimizationAndCompression | bytes}} compressed)
-{{file.gain | bytes}}
diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 4ab47be..358b748 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -770,7 +770,7 @@ var policies = { "imageOptimization": { "tool": "redownload", "label": "Image optimization", - "message": "

This metric measures the number of bytes that could be saved by optimizing images.

Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as Kraken.io or the excellent ImageOptim on Mac. For SVG images, you can use SVGOMG

The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!

", + "message": "

This metric measures the number of bytes that could be saved by optimizing images.

Image optimization is generally one of the easiest way to reduce a page weight, and as a result, the page load time. Don't use Photoshop or other image editing tools, they're not very good for optimization. Use specialized tools such as Kraken.io or the excellent ImageOptim on Mac. For SVG images, you can use SVGOMG.

The tools in use in YellowLabTools are not set to their maximum optimization power (JPEG quality 85), so you might be able to compress even more!

", "isOkThreshold": 20480, "isBadThreshold": 204800, "isAbnormalThreshold": 307200, @@ -788,8 +788,8 @@ var policies = { }, "compression": { "tool": "redownload", - "label": "Gzip/Brotli compression", - "message": "

Measures the number of bytes that could be saved by compressing textual files.

Gzip is a good old weight reducer. But Brotli is a new-generation compression tool and it gives better results. All major server systems are now compatible with Brotli.

Note that compressing small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. Here is a list of Content-Types that should be compressed.

", + "label": "Brotli compression", + "message": "

Measures the number of bytes that could be saved by compressing textual files.

Gzip is a good old algorithm that offers great improvements. But Brotli is a new-generation algorithm and provides even better results. All major server systems are now compatible with Brotli.

Note that compressing small files (< 1 KB) is arguable, and that some assets such as images should not be gzipped as they are already compressed. Here is a list of Content-Types that should be compressed.

", "isOkThreshold": 20480, "isBadThreshold": 204800, "isAbnormalThreshold": 409600, diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index 89ceff9..ee07e10 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -340,9 +340,34 @@ var Redownload = function() { }; requests.forEach(function(req) { + if (req.weightCheck.bodySize > 0 && imageOptimizer.entryTypeCanBeOptimized(req) && req.weightCheck.isOptimized === false) { - var before = req.weightCheck.afterGzipCompression || req.weightCheck.bodySize; - var after = req.weightCheck.afterOptimizationAndGzipCompression || req.weightCheck.optimized; + + var before, after; + + if (req.weightCheck.isCompressed === true) { + // The resource is compressed + + before = req.weightCheck.bodySize; + + if (req.weightCheck.compressionTool === 'brotli') { + after = req.weightCheck.afterOptimizationAndBrotliCompression; + } else { + after = req.weightCheck.afterOptimizationAndGzipCompression; + } + + } else if (req.weightCheck.afterBrotliCompression) { + // The resource is not compressed but should be + + before = req.weightCheck.afterBrotliCompression; + after = req.weightCheck.afterOptimizationAndBrotliCompression; + } else { + // The resource is not compressed but is not subject to compression + + before = req.weightCheck.bodySize; + after = req.weightCheck.optimized; + } + var gain = before - after; if (gain > 200) { @@ -350,10 +375,11 @@ var Redownload = function() { results.images.push({ url: req.url, - original: req.weightCheck.bodySize, + originalWeigth: req.weightCheck.bodySize, + isCompressible: (req.weightCheck.afterBrotliCompression > 0), isCompressed: req.weightCheck.isCompressed, - afterGzipCompression: req.weightCheck.afterGzipCompression, - afterOptimizationAndGzipCompression: req.weightCheck.afterOptimizationAndGzipCompression, + originalCompressedWeight: before, + afterOptimizationAndCompression: after, lossless: req.weightCheck.lossless, lossy: req.weightCheck.lossy, gain: gain @@ -361,7 +387,6 @@ var Redownload = function() { } } }); - return results; } @@ -405,15 +430,43 @@ var Redownload = function() { var after = req.weightCheck.afterOptimizationAndGzipCompression || req.weightCheck.optimized; var gain = before - after; + var before, after; + + if (req.weightCheck.isCompressed === true) { + // The resource is compressed + + before = req.weightCheck.bodySize; + + if (req.weightCheck.compressionTool === 'brotli') { + after = req.weightCheck.afterOptimizationAndBrotliCompression; + } else { + after = req.weightCheck.afterOptimizationAndGzipCompression; + } + + } else if (req.weightCheck.afterBrotliCompression) { + // The resource is not compressed but should be + + before = req.weightCheck.afterBrotliCompression; + after = req.weightCheck.afterOptimizationAndBrotliCompression; + } else { + // The resource is not compressed but is not subject to compression + + before = req.weightCheck.bodySize; + after = req.weightCheck.optimized; + } + + var gain = before - after; + if (gain > 200) { results.totalGain += gain; results.files.push({ url: req.url, - original: req.weightCheck.bodySize, + originalWeigth: req.weightCheck.bodySize, + isCompressible: (req.weightCheck.afterBrotliCompression > 0), isCompressed: req.weightCheck.isCompressed, - afterGzipCompression: req.weightCheck.afterGzipCompression, - afterOptimizationAndGzipCompression: req.weightCheck.afterOptimizationAndGzipCompression, + originalCompressedWeight: before, + afterOptimizationAndCompression: after, optimized: req.weightCheck.optimized, gain: gain }); From 1e5f7971952693c249b1cd7ee3730afa8a9a8389 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 22 Dec 2020 12:27:35 +0100 Subject: [PATCH 41/59] Fix http2 offenders page --- front/src/views/rule.html | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/front/src/views/rule.html b/front/src/views/rule.html index d2c8c77..201a7d4 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -176,7 +176,7 @@ ({{offender.size | bytes}})
-
+
{{offender.domain}} receives over {{offender.httpVersion}}
@@ -511,18 +511,6 @@
-
-

Protocols advertised by the server

-
-
-
{{protocol}}
-
-
-
none
-
-
-
-

404

Rule "{{policyName}}"" not found From df4f5685bea31c590d02e54a2465a65c54873354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Wed, 23 Dec 2020 15:24:48 +0100 Subject: [PATCH 42/59] New metric Total JS execution time --- front/src/views/dashboard.html | 2 +- front/src/views/rule.html | 4 ++-- lib/metadata/policies.js | 10 ++++++++++ lib/metadata/scoreProfileGeneric.json | 7 ++++--- 4 files changed, 17 insertions(+), 6 deletions(-) diff --git a/front/src/views/dashboard.html b/front/src/views/dashboard.html index cdce76d..e38ce19 100644 --- a/front/src/views/dashboard.html +++ b/front/src/views/dashboard.html @@ -43,7 +43,7 @@
{{rule.policy.label}}
{{rule.value | bytes}} - {{rule.value}} + {{rule.value}} {{rule.policy.unit}} diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 201a7d4..245dbe0 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -12,12 +12,12 @@

Value: {{rule.value | bytes}} - {{rule.value}} + {{rule.value}} {{rule.policy.unit}}

Have {{rule.policy.isOkThreshold | bytes}} - {{rule.policy.isOkThreshold}} + {{rule.policy.isOkThreshold}} {{rule.policy.unit}} or less or more to get the 100/100 score. diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 358b748..bbc61b9 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -46,6 +46,16 @@ var policies = { "isAbnormalThreshold": 50, "hasOffenders": true }, + "scriptDuration": { + "tool": "phantomas", + "label": "Total JS execution time", + "message": "

This is the number of milliseconds spent by the browser on JavaScript execution during page load.

For more details, try using the performance tab in Chrome DevTools. It is a bit complicated at first sight, but you'll be able to analyze exactly where this execution time is spent.

", + "isOkThreshold": 500, + "isBadThreshold": 2000, + "isAbnormalThreshold": 4000, + "hasOffenders": false, + "unit": 'ms' + }, "DOMaccesses": { "tool": "domAccessAgregator", "label": "DOM access", diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 3131fef..3fbbe03 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -13,7 +13,7 @@ "requests": { "label": "Requests", "policies": { - "totalRequests": 5, + "totalRequests": 2, "domains": 3, "notFound": 3, "identicalFiles": 2, @@ -35,7 +35,8 @@ "javascriptComplexity": { "label": "JS complexity", "policies": { - "DOMaccesses": 4, + "scriptDuration": 4, + "DOMaccesses": 3, "eventsScrollBound": 1 } }, @@ -105,7 +106,7 @@ }, "globalScore": { "pageWeight": 3, - "requests": 3, + "requests": 2, "domComplexity": 2, "javascriptComplexity": 2, "badJavascript": 2, From 4396bd462552e6415c822ba020d496c00595d594 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 29 Dec 2020 11:55:22 +0100 Subject: [PATCH 43/59] White background not needed & optimize images more --- lib/screenshotHandler.js | 37 +------------------------------------ 1 file changed, 1 insertion(+), 36 deletions(-) diff --git a/lib/screenshotHandler.js b/lib/screenshotHandler.js index 29d68a8..3632236 100644 --- a/lib/screenshotHandler.js +++ b/lib/screenshotHandler.js @@ -25,8 +25,6 @@ var screenshotHandler = function() { return that.resizeImage(image, width); }) - .then(this.addWhiteBackground) - .then(this.toBuffer); }; @@ -76,44 +74,11 @@ var screenshotHandler = function() { return deferred.promise; }; - // If the page doesn't set a bg color, the default PhantomJS one is transparent - // When transforming PNG to JPG, transparent pixels become black. - // This is why we need to add a transparent background. - this.addWhiteBackground = function(image) { - var deferred = Q.defer(); - - // Create a canvas with the same dimensions as your image: - new Jimp(image.bitmap.width, image.bitmap.height, 0xFFFFFF, function(err, canvas){ - if (err) { - debug('Could not create a white canvas'); - debug(err); - - deferred.reject(err); - } else { - // Paste original image on top of the canvas - canvas.composite(image, 0, 0, function(err, image){ - if (err) { - debug('Could not paste image on the white canvas'); - debug(err); - - deferred.reject(err); - } else { - // Now image has a white background... - debug('White background correctly added'); - deferred.resolve(image); - } - }); - } - }); - - return deferred.promise; - }; - this.toBuffer = function(image) { var deferred = Q.defer(); - image.quality(90).getBuffer(Jimp.MIME_JPEG, function(err, buffer){ + image.quality(85).getBuffer(Jimp.MIME_JPEG, function(err, buffer){ if (err) { debug('Could not save image to buffer'); debug(err); From 57616aba62fd80b29d096311d04587af0a630fb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 29 Dec 2020 11:55:57 +0100 Subject: [PATCH 44/59] Fix wording --- front/src/views/rule.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 245dbe0..b2ee64b 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -177,7 +177,7 @@
- {{offender.domain}} receives over {{offender.httpVersion}} + {{offender.domain}} sends over {{offender.httpVersion}}
From 65a2bbae0ad5b37c41145fa9fa8c99533a56f4c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 29 Dec 2020 11:56:54 +0100 Subject: [PATCH 45/59] Adjust scores to nowadays browsers and network --- lib/metadata/policies.js | 135 ++++++++++++-------------- lib/metadata/scoreProfileGeneric.json | 17 ++-- 2 files changed, 71 insertions(+), 81 deletions(-) diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index bbc61b9..891daf5 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -15,9 +15,9 @@ var policies = { "tool": "phantomas", "label": "DOM max depth", "message": "

A deep DOM makes the CSS matching with DOM elements difficult.

It also slows down JavaScript modifications to the DOM because changing the dimensions of an element makes the browser re-calculate the dimensions of it's parents. Same thing for JavaScript events, that bubble up to the document root.

", - "isOkThreshold": 12, - "isBadThreshold": 22, - "isAbnormalThreshold": 30, + "isOkThreshold": 14, + "isBadThreshold": 24, + "isAbnormalThreshold": 32, "hasOffenders": true, "offendersTransformFn": function(offenders) { var domArrays = offenders.map(offendersHelpers.domPathToArray); @@ -32,7 +32,7 @@ var policies = { "tool": "phantomas", "label": "Number of iframes", "message": "

iFrames are the most complex HTML elements. They are pages, just like the main page, and the browser needs to create a new page context, which has a cost.

", - "isOkThreshold": 3, + "isOkThreshold": 4, "isBadThreshold": 15, "isAbnormalThreshold": 30, "hasOffenders": true @@ -42,7 +42,7 @@ var policies = { "label": "IDs duplicated", "message": "

IDs of HTML elements must be document-wide unique. This can cause problems with getElementById returning the wrong element.

", "isOkThreshold": 0, - "isBadThreshold": 5, + "isBadThreshold": 10, "isAbnormalThreshold": 50, "hasOffenders": true }, @@ -102,7 +102,7 @@ var policies = { "message": "

Just to let you know there are some errors on the page.

", "isOkThreshold": 0, "isBadThreshold": 1, - "isAbnormalThreshold": 4, + "isAbnormalThreshold": 5, "hasOffenders": true, "offendersTransformFn": function(offenders) { return { @@ -132,8 +132,8 @@ var policies = { "label": "document.write calls", "message": "

They slow down the page construction, especially if they are used to insert scripts in the page. Remove them ASAP.

If you cannot remove them because they come from a third-party script (such as ads), have a look at PostScribe.

", "isOkThreshold": 0, - "isBadThreshold": 3, - "isAbnormalThreshold": 8, + "isBadThreshold": 2, + "isAbnormalThreshold": 6, "hasOffenders": true, "offendersTransformFn": function(offenders) { return { @@ -187,22 +187,13 @@ var policies = { "isAbnormalThreshold": 1, "hasOffenders": true }, - "consoleMessages": { - "tool": "phantomas", - "label": "Console messages", - "message": "

Try to keep your console clean when in production. Debugging is good for development only.

Writing in the console has a cost, especially when dumping large object variables.

There is also a problem with Internet Explorer 8, not knowing the console object.

", - "isOkThreshold": 3, - "isBadThreshold": 20, - "isAbnormalThreshold": 50, - "hasOffenders": false - }, "globalVariables": { "tool": "phantomas", "label": "Global variables", "message": "

It is a bad practice because they clutter up the global namespace. If two scripts use the same variable name in the global scope, it can cause conflicts and it is generally hard to debug.

Global variables also take a (very) little bit longer to be accessed than variables in the local scope of a function.

", - "isOkThreshold": 40, - "isBadThreshold": 200, - "isAbnormalThreshold": 700, + "isOkThreshold": 20, + "isBadThreshold": 300, + "isAbnormalThreshold": 800, "hasOffenders": true, "offendersTransformFn": function(offenders) { return { @@ -282,7 +273,7 @@ var policies = { "message": "

jQuery is a heavy library. You should never load jQuery more than once on the same page.

", "isOkThreshold": 1, "isBadThreshold": 2, - "isAbnormalThreshold": 2, + "isAbnormalThreshold": 3, "hasOffenders": true }, "cssParsingErrors": { @@ -290,7 +281,7 @@ var policies = { "label": "CSS syntax error", "message": "

Yellow Lab Tools failed to parse a CSS file. I doubt the problem comes from the css parser.

Maybe a CSS validator can help you.

", "isOkThreshold": 0, - "isBadThreshold": 1, + "isBadThreshold": 2, "isAbnormalThreshold": 20, "hasOffenders": true, "offendersTransformFn": function(offenders) { @@ -329,9 +320,9 @@ var policies = { "tool": "phantomas", "label": "Rules count", "message": "

Having a huge number of CSS rules hurts performances. If the number of CSS rules is higher than the number of DOM elements, there is clearly a problem.

Huge stylesheets generally occur when the different pages of a website load all the CSS, concatenated in a single stylesheet, even if a large part of the rules are page-specific. Solution is to create one main CSS file with global rules and one custom file per page.

", - "isOkThreshold": 1000, - "isBadThreshold": 3000, - "isAbnormalThreshold": 4500, + "isOkThreshold": 2000, + "isBadThreshold": 5000, + "isAbnormalThreshold": 8000, "hasOffenders": true }, "cssComplexSelectors": { @@ -339,8 +330,8 @@ var policies = { "label": "Complex selectors", "message": "

Complex selectors are CSS selectors with 4 or more expressions, like \"#header ul li .foo\".

They are adding more work for the browser, and this could be avoided by simplifying selectors. The B.E.M. methodology is a useful way to simplify your CSS.

", "isOkThreshold": 0, - "isBadThreshold": 800, - "isAbnormalThreshold": 2000, + "isBadThreshold": 1000, + "isAbnormalThreshold": 24000, "hasOffenders": true, "offendersTransformFn": function(offenders) { var parsedOffenders = offenders.map(function(offender) { @@ -355,9 +346,9 @@ var policies = { "tool": "phantomas", "label": "Colors count", "message": "

This is the number of different colors defined in CSS.

Your CSS will be easier to maintain if you keep a small color set.

", - "isOkThreshold": 50, - "isBadThreshold": 150, - "isAbnormalThreshold": 400, + "isOkThreshold": 100, + "isBadThreshold": 200, + "isAbnormalThreshold": 500, "hasOffenders": true, "offendersTransformFn": function(offenders, ruleObject) { var deduplicatedObj = {}; @@ -402,8 +393,8 @@ var policies = { "label": "Similar colors", "message": "

This is the list of colors found in the stylesheets, that are very close to each other. The eye can barely see the difference.

Use this list to reduce the number of colors in your palette, it will be easier to maintain.

", "isOkThreshold": 0, - "isBadThreshold": 40, - "isAbnormalThreshold": 80, + "isBadThreshold": 60, + "isAbnormalThreshold": 120, "hasOffenders": true, "offendersTransformFn": function(offenders) { return { @@ -416,9 +407,9 @@ var policies = { "tool": "mediaQueriesChecker", "label": "Breakpoints count", "message": "

This is the number of different breakpoints found in the stylesheets' media queries.

Please note this rule is based on min-width, max-width, min-device-width and max-device-width media queries only.

Your CSS will be easier to maintain if you keep a reasonable number of breakpoints. Try to make a fluid design - using percents - to avoid the creation of numerous breakpoints.

", - "isOkThreshold": 6, - "isBadThreshold": 40, - "isAbnormalThreshold": 60, + "isOkThreshold": 10, + "isBadThreshold": 50, + "isAbnormalThreshold": 80, "hasOffenders": true, "offendersTransformFn": function(offenders) { var offendersTable = []; @@ -438,9 +429,9 @@ var policies = { "tool": "mediaQueriesChecker", "label": "Not mobile-first media queries", "message": "

This is the number of CSS rules inside media queries that address small screens.

The common good practice, when creating a responsive website, is to write it \"mobile-first\". More explanation in this great article.

", - "isOkThreshold": 50, - "isBadThreshold": 250, - "isAbnormalThreshold": 1000, + "isOkThreshold": 60, + "isBadThreshold": 400, + "isAbnormalThreshold": 1200, "hasOffenders": true, "offendersTransformFn": function(offenders) { return offendersHelpers.orderByFile(offenders); @@ -469,8 +460,8 @@ var policies = { "label": "Duplicated selectors", "message": "

This is when two or more selectors are strictly identical and should be merged.

", "isOkThreshold": 0, - "isBadThreshold": 50, - "isAbnormalThreshold": 100, + "isBadThreshold": 100, + "isAbnormalThreshold": 200, "hasOffenders": true, "offendersTransformFn": function(offenders) { var parsedOffenders = offenders.map(function(offender) { @@ -498,8 +489,8 @@ var policies = { "label": "Duplicated properties", "message": "

This is the number of property definitions duplicated within a selector.

", "isOkThreshold": 0, - "isBadThreshold": 60, - "isAbnormalThreshold": 120, + "isBadThreshold": 100, + "isAbnormalThreshold": 200, "hasOffenders": true, "offendersTransformFn": function(offenders) { var parsedOffenders = offenders.map(function(offender) { @@ -532,7 +523,7 @@ var policies = { "message": "

Very easy to fix: remove all empty rules.

", "isOkThreshold": 0, "isBadThreshold": 50, - "isAbnormalThreshold": 100, + "isAbnormalThreshold": 120, "hasOffenders": true, "offendersTransformFn": function(offenders) { var parsedOffenders = offenders.map(offendersHelpers.cssOffenderPattern); @@ -578,8 +569,8 @@ var policies = { "label": "Uses of !important", "message": "

It can be useful, but only as a last resort. It is a bad practice because it overrides the normal cascading logic. The more you use !important, the more you need it again to over-override. This conducts to a poor maintainability.

", "isOkThreshold": 0, - "isBadThreshold": 75, - "isAbnormalThreshold": 200, + "isBadThreshold": 200, + "isAbnormalThreshold": 500, "hasOffenders": true, "offendersTransformFn": function(offenders) { var parsedOffenders = offenders.map(function(offender) { @@ -612,8 +603,8 @@ var policies = { "label": "Old IE fixes", "message": "

What browser do you need to support? Once you've got the answer, take a look at these old rules that pollute your CSS code and remove them.

IE6:

  • * html
  • html > body (everything but IE6)

IE7:

  • *height: 123px;
  • height: 123px !ie;

IE9:

  • -ms-filter
  • progid:DXImageTransform.Microsoft

", "isOkThreshold": 0, - "isBadThreshold": 50, - "isAbnormalThreshold": 150, + "isBadThreshold": 100, + "isAbnormalThreshold": 300, "hasOffenders": true, "offendersTransformFn": function(offenders) { var parsedOffenders = offenders.map(function(offender) { @@ -668,8 +659,8 @@ var policies = { "label": "Old prefixes", "message": "

Many property prefixes such as -moz- or -webkit- are not needed anymore, or by very few people. Sometimes, they have never even existed. You can remove them or replace them with the non-prefixed version. This will help reducing your stylesheets weight.

The prefixes database comes from Can I Use.

", "isOkThreshold": 0, - "isBadThreshold": 50, - "isAbnormalThreshold": 200, + "isBadThreshold": 100, + "isAbnormalThreshold": 400, "hasOffenders": true, "offendersTransformFn": function(offenders) { var properties = {}; @@ -721,8 +712,8 @@ var policies = { "label": "Redundant body selectors", "message": "

This is one way to remove complexity from a CSS rule. Generally, when \"body\" is specified in a rule it can be removed, because an element is necessarily inside the body.

", "isOkThreshold": 0, - "isBadThreshold": 60, - "isAbnormalThreshold": 200, + "isBadThreshold": 120, + "isAbnormalThreshold": 400, "hasOffenders": true, "offendersTransformFn": function(offenders) { var parsedOffenders = offenders.map(function(offender) { @@ -739,8 +730,8 @@ var policies = { "label": "Redundant tags selectors", "message": "

Some tags included inside other tags are obvious. For example, when \"ul li\" is specified in a rule, \"ul\" can be removed because the \"li\" tag is nearly always inside an \"ul\" container (the \"ol\" container is quite rare). Same thing for \"tr td\", \"select option\", ...

Lowering compexity in CSS selectors can make the page load a little faster.

", "isOkThreshold": 0, - "isBadThreshold": 60, - "isAbnormalThreshold": 200, + "isBadThreshold": 120, + "isAbnormalThreshold": 400, "hasOffenders": true, "offendersTransformFn": function(offenders) { var parsedOffenders = offenders.map(function(offender) { @@ -771,9 +762,9 @@ var policies = { "tool": "redownload", "label": "Total weight", "message": "

The weight is of course very important if you want the page to load fast. Try to stay under 1MB, which is already very long to download over a slow connection.

", - "isOkThreshold": 1048576, - "isBadThreshold": 2621440, - "isAbnormalThreshold": 4194304, + "isOkThreshold": 1572864, + "isBadThreshold": 3145728, + "isAbnormalThreshold": 5242880, "hasOffenders": true, "unit": 'bytes' }, @@ -819,19 +810,19 @@ var policies = { "totalRequests": { "tool": "redownload", "label": "Requests number", - "message": "

This is one of the most important performance rule. Every request is slowing down the page loading.

There are several technics to reduce their number:

  • Concatenate JS files
  • Concatenate CSS files
  • Embed or inline small JS or CSS files in the HTML
  • Create sprites
  • Base64 encode small images in HTML or stylesheets
  • Use lazyloading for images

", - "isOkThreshold": 20, - "isBadThreshold": 120, - "isAbnormalThreshold": 200, + "message": "

Each request slows down the page loading, especially on the protocol HTTP/1, but also on HTTP/2.

There are several technics to reduce their number:

  • Concatenate JS files
  • Concatenate CSS files
  • Embed or inline small JS or CSS files in the HTML
  • Create sprites
  • Base64 encode small images in HTML or stylesheets
  • Use lazyloading for images

", + "isOkThreshold": 50, + "isBadThreshold": 150, + "isAbnormalThreshold": 250, "hasOffenders": true }, "domains": { "tool": "phantomas", "label": "Different domains", "message": "

For each domain met, the browser needs to make a DNS look-up, which is slow. Avoid having to many different domains and the page should render faster.

By the way, domain sharding is not a good practice anymore.

", - "isOkThreshold": 10, - "isBadThreshold": 25, - "isAbnormalThreshold": 50, + "isOkThreshold": 12, + "isBadThreshold": 30, + "isAbnormalThreshold": 60, "hasOffenders": true }, "notFound": { @@ -877,9 +868,9 @@ var policies = { "tool": "redownload", "label": "Small requests", "message": "

List of all requests that are less than 2 KB. Try to merge them with other files.

", - "isOkThreshold": 10, - "isBadThreshold": 50, - "isAbnormalThreshold": 80, + "isOkThreshold": 20, + "isBadThreshold": 60, + "isAbnormalThreshold": 90, "hasOffenders": true }, "lazyLoadableImagesBelowTheFold": { @@ -906,7 +897,7 @@ var policies = { "message": "

This is the number of custom web fonts loaded on the page.

Webfonts are beautiful, but heavy. You should keep their number as low as possible.

", "isOkThreshold": 1, "isBadThreshold": 5, - "isAbnormalThreshold": 7, + "isAbnormalThreshold": 8, "hasOffenders": true, "offendersTransformFn": function(offenders) { return offenders; @@ -1000,9 +991,9 @@ var policies = { "tool": "phantomas", "label": "Caching disabled", "message": "

Counts responses with caching disabled (max-age=0)

Fix immediatly if on static assets.

", - "isOkThreshold": 0, - "isBadThreshold": 12, - "isAbnormalThreshold": 25, + "isOkThreshold": 2, + "isBadThreshold": 15, + "isAbnormalThreshold": 30, "hasOffenders": true }, "cachingNotSpecified": { @@ -1019,8 +1010,8 @@ var policies = { "label": "Caching too short", "message": "

Responses with too short caching time (less than a week).

The longer you cache, the better. Add versionning to your static assets, if it's not already done, and set their cache time to one year.

", "isOkThreshold": 5, - "isBadThreshold": 20, - "isAbnormalThreshold": 40, + "isBadThreshold": 25, + "isAbnormalThreshold": 50, "hasOffenders": true, "offendersTransformFn": function(offenders) { return { diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 3fbbe03..64a00ee 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -7,7 +7,7 @@ "imageOptimization": 2, "imagesTooLarge": 1, "compression": 2, - "fileMinification": 1 + "fileMinification": 2 } }, "requests": { @@ -18,7 +18,7 @@ "notFound": 3, "identicalFiles": 2, "emptyRequests": 3, - "smallRequests": 1, + "smallRequests": 0.5, "lazyLoadableImagesBelowTheFold": 2, "hiddenImages": 1 } @@ -36,7 +36,7 @@ "label": "JS complexity", "policies": { "scriptDuration": 4, - "DOMaccesses": 3, + "DOMaccesses": 2, "eventsScrollBound": 1 } }, @@ -46,7 +46,6 @@ "jsErrors": 1, "documentWriteCalls": 2, "synchronousXHR": 5, - "consoleMessages": 0.5, "globalVariables": 0.5 } }, @@ -71,13 +70,13 @@ "badCSS": { "label": "Bad CSS", "policies": { - "cssParsingErrors": 4, - "cssImports": 3, + "cssParsingErrors": 3, + "cssImports": 4, "cssDuplicatedSelectors": 2, "cssDuplicatedProperties": 1, "cssEmptyRules": 2, "cssExpressions": 1, - "cssImportants": 3, + "cssImportants": 2, "cssOldIEFixes": 1, "cssOldPropertyPrefixes": 1, "cssRedundantBodySelectors": 1, @@ -110,8 +109,8 @@ "domComplexity": 2, "javascriptComplexity": 2, "badJavascript": 2, - "jQuery": 1, - "cssComplexity": 1, + "jQuery": 0.5, + "cssComplexity": 0.5, "badCSS": 1, "fonts": 1, "serverConfig": 1 From 4c1c375eefd5bdc45ca4a324834864a13d7e40cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 29 Dec 2020 22:45:29 +0100 Subject: [PATCH 46/59] Fix tests starting before the end of the previous one --- lib/server/controllers/apiController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/server/controllers/apiController.js b/lib/server/controllers/apiController.js index fc41f0a..d6f701f 100644 --- a/lib/server/controllers/apiController.js +++ b/lib/server/controllers/apiController.js @@ -127,7 +127,7 @@ var ApiController = function(app) { } // Let's continue - screenshotPromise + return screenshotPromise // Save results .then(function() { From e8d464a24438d5f4d3bccdb972c6daaed7f1ffc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Wed, 30 Dec 2020 00:21:29 +0100 Subject: [PATCH 47/59] If you fix this rule, your global score increases by X points --- front/src/views/rule.html | 11 ++++++++++- lib/runner.js | 20 +++++++++++++++++++- lib/scoreCalculator.js | 14 +++++++++----- 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/front/src/views/rule.html b/front/src/views/rule.html index b2ee64b..7961171 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -20,7 +20,16 @@ {{rule.policy.isOkThreshold}} {{rule.policy.unit}} or less or more - to get the 100/100 score. + to get the 100/100 score on this issue. +
+
+ Your new global would increase by {{rule.globalScoreIfFixed - result.scoreProfiles.generic.globalScore}} points ({{rule.globalScoreIfFixed}}/100). +
+
+ Your new global score would increase, but still not enough to reach 0/100. That's embarassing... +
+
+ Your new global score would slightly increase, but not enough to gain a single point.
diff --git a/lib/runner.js b/lib/runner.js index 93255e8..31e2044 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -51,10 +51,28 @@ var Runner = function(params) { // Scores calculator var scoreProfileGeneric = require('./metadata/scoreProfileGeneric.json'); data.scoreProfiles = { - generic : scoreCalculator.calculate(data, scoreProfileGeneric) + generic : scoreCalculator.calculate(data, scoreProfileGeneric, true) }; + // Calculate "If you fix this issue, your new score is..." on each rule + debug('Calculate "If you fix this issue..." scores'); + Object.keys(data.rules).forEach(function(ruleName) { + // Save current values + var oldScore = data.rules[ruleName].score; + var oldAbnormalityScore = data.rules[ruleName].abnormalityScore; + // Simulate a 100/100 score on a specific rule + data.rules[ruleName].score = 100; + data.rules[ruleName].abnormalityScore = 0; + // Calculate new score + data.rules[ruleName].globalScoreIfFixed = scoreCalculator.calculate(data, scoreProfileGeneric, false).globalScore; + // Revert values + data.rules[ruleName].score = oldScore; + data.rules[ruleName].abnormalityScore = oldAbnormalityScore; + }); + debug('Calculating is finished'); + + if (data.toolsResults.phantomas.offenders.blockedRequests) { data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests; } diff --git a/lib/scoreCalculator.js b/lib/scoreCalculator.js index 2d8dfaf..3fde310 100644 --- a/lib/scoreCalculator.js +++ b/lib/scoreCalculator.js @@ -4,7 +4,7 @@ var debug = require('debug')('ylt:scoreCalculator'); var ScoreCalculator = function() { 'use strict'; - this.calculate = function(data, profile) { + this.calculate = function(data, profile, debugFlag) { var results = { categories: {} @@ -13,7 +13,9 @@ var ScoreCalculator = function() { var categoryName; var weight; - debug('Starting calculating scores'); + if (debugFlag) { + debug('Starting calculating scores'); + } // Calculate categories for (categoryName in profile.categories) { @@ -31,7 +33,7 @@ var ScoreCalculator = function() { if (data.rules[policyName]) { policyScore = data.rules[policyName].score + (data.rules[policyName].abnormalityScore * 2); categoryScore.push(policyScore, weight); - } else { + } else if (debugFlag) { debug('Warning: could not find rule %s', policyName); } @@ -59,8 +61,10 @@ var ScoreCalculator = function() { results.globalScore = Math.round(globalScore.getScore()); - debug('Score calculation finished:'); - debug(results); + if (debugFlag) { + debug('Score calculation finished:'); + debug(results); + } return results; }; From 4193d61e0bc08605119a20518dafbfb2cc8c02d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Wed, 30 Dec 2020 00:26:36 +0100 Subject: [PATCH 48/59] Remove uselss code --- lib/tools/redownload/redownload.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index ee07e10..0876272 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -426,10 +426,6 @@ var Redownload = function() { requests.forEach(function(req) { if (req.weightCheck.bodySize > 0 && fileMinifier.entryTypeCanBeMinified(req) && req.weightCheck.isOptimized === false) { - var before = req.weightCheck.afterGzipCompression || req.weightCheck.bodySize; - var after = req.weightCheck.afterOptimizationAndGzipCompression || req.weightCheck.optimized; - var gain = before - after; - var before, after; if (req.weightCheck.isCompressed === true) { From 3f098cd33ac21c061e514c663a9954cd516270a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Wed, 30 Dec 2020 00:37:33 +0100 Subject: [PATCH 49/59] Give less score weight to requests number --- lib/metadata/policies.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 891daf5..4231be3 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -810,10 +810,10 @@ var policies = { "totalRequests": { "tool": "redownload", "label": "Requests number", - "message": "

Each request slows down the page loading, especially on the protocol HTTP/1, but also on HTTP/2.

There are several technics to reduce their number:

  • Concatenate JS files
  • Concatenate CSS files
  • Embed or inline small JS or CSS files in the HTML
  • Create sprites
  • Base64 encode small images in HTML or stylesheets
  • Use lazyloading for images

", - "isOkThreshold": 50, - "isBadThreshold": 150, - "isAbnormalThreshold": 250, + "message": "

Each request slows down the page loading, especially on the protocol HTTP/1, but also a little on HTTP/2.

There are several technics to reduce their number:

  • Concatenate JS files
  • Concatenate CSS files
  • Embed or inline small JS or CSS files in the HTML
  • Create sprites
  • Base64 encode small images in HTML or stylesheets
  • Use lazyloading for images

", + "isOkThreshold": 80, + "isBadThreshold": 240, + "isAbnormalThreshold": 320, "hasOffenders": true }, "domains": { From 834a9c9ac585ff7a06373bc9c986386145c7aeed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Wed, 30 Dec 2020 00:39:56 +0100 Subject: [PATCH 50/59] Remove the Small Requests rule --- front/src/views/rule.html | 21 ------------------ lib/metadata/policies.js | 9 -------- lib/metadata/scoreProfileGeneric.json | 1 - lib/tools/redownload/redownload.js | 31 +-------------------------- test/core/redownloadTest.js | 6 ------ 5 files changed, 1 insertion(+), 67 deletions(-) diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 7961171..7effe8b 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -409,27 +409,6 @@
-
-
-

-

Try to inline these styles in the head of the HTML or to merge them with other files.

-

Try to inline these scripts in the HTML or merge them with other files.

-

Try to inline these images (with base64 encoding for most image types except SVG that don't need base64 encoding). You can also create sprites.

-
-
-
Preview
-
File
-
Weight (bytes)
-
-
-
-
-
{{request.size}}
-
-
-
-
-

Breakpoints list

diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 4231be3..3a3591f 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -864,15 +864,6 @@ var policies = { "isAbnormalThreshold": 5, "hasOffenders": true }, - "smallRequests": { - "tool": "redownload", - "label": "Small requests", - "message": "

List of all requests that are less than 2 KB. Try to merge them with other files.

", - "isOkThreshold": 20, - "isBadThreshold": 60, - "isAbnormalThreshold": 90, - "hasOffenders": true - }, "lazyLoadableImagesBelowTheFold": { "tool": "phantomas", "label": "Below the fold images", diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 64a00ee..953b2f8 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -18,7 +18,6 @@ "notFound": 3, "identicalFiles": 2, "emptyRequests": 3, - "smallRequests": 0.5, "lazyLoadableImagesBelowTheFold": 2, "hiddenImages": 1 } diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index 0876272..f7169c3 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -137,7 +137,7 @@ var Redownload = function() { offenders.emptyRequests = listEmptyRequests(results); metrics.emptyRequests = offenders.emptyRequests.length; - // Now remove unwanted responses (redirections and empty files) + // Remove some more unwanted responses (redirections and empty files) results = results.filter(function(result) { return (/* (result.status < 300 || result.status >= 400) && */ result.weightCheck.bodySize > 0); }); @@ -158,10 +158,6 @@ var Redownload = function() { offenders.compression = listFilesNotBrotlified(results); metrics.compression = offenders.compression.totalGain; - // Small requests - offenders.smallRequests = listSmallRequests(results); - metrics.smallRequests = offenders.smallRequests.total; - // Detect identical files offenders.identicalFiles = listIdenticalFiles(results); metrics.identicalFiles = offenders.identicalFiles.avoidableRequests; @@ -538,31 +534,6 @@ var Redownload = function() { return results; } - function listSmallRequests(requests) { - var results = { - total: 0, - byType: { - css: [], - js: [], - image: [] - } - }; - - requests.forEach(function(req) { - if (req.weightCheck.bodySize > 0 && req.weightCheck.bodySize < 2048) { - if (req.isCSS || req.isJS || req.isImage) { - results.byType[req.type].push({ - url: req.url, - size: req.weightCheck.bodySize - }); - results.total ++; - } - } - }); - - return results; - } - function listIdenticalFiles(requests) { var hashes = {}; var list = []; diff --git a/test/core/redownloadTest.js b/test/core/redownloadTest.js index 9c200be..c1964b4 100644 --- a/test/core/redownloadTest.js +++ b/test/core/redownloadTest.js @@ -108,12 +108,6 @@ describe('redownload', function() { data.toolsResults.redownload.offenders.totalRequests.byType.video.length.should.equal(0); data.toolsResults.redownload.offenders.totalRequests.byType.other.length.should.equal(1); - data.toolsResults.redownload.metrics.should.have.a.property('smallRequests').that.equals(0); - data.toolsResults.redownload.offenders.should.have.a.property('smallRequests'); - data.toolsResults.redownload.offenders.smallRequests.byType.js.length.should.equal(0); - data.toolsResults.redownload.offenders.smallRequests.byType.css.length.should.equal(0); - data.toolsResults.redownload.offenders.smallRequests.byType.image.length.should.equal(0); - done(); }) From 7e9b12f818844d717ca03d9d3388bb6911ad21ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Wed, 30 Dec 2020 00:52:18 +0100 Subject: [PATCH 51/59] Adjust scoring --- front/src/views/rule.html | 5 ++++- lib/metadata/scoreProfileGeneric.json | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 7effe8b..98b3bb3 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -22,9 +22,12 @@ or more to get the 100/100 score on this issue.
-
+
Your new global would increase by {{rule.globalScoreIfFixed - result.scoreProfiles.generic.globalScore}} points ({{rule.globalScoreIfFixed}}/100).
+
+ Your new global would increase by {{rule.globalScoreIfFixed}} points ({{rule.globalScoreIfFixed}}/100). +
Your new global score would increase, but still not enough to reach 0/100. That's embarassing...
diff --git a/lib/metadata/scoreProfileGeneric.json b/lib/metadata/scoreProfileGeneric.json index 953b2f8..080a18c 100644 --- a/lib/metadata/scoreProfileGeneric.json +++ b/lib/metadata/scoreProfileGeneric.json @@ -15,7 +15,7 @@ "policies": { "totalRequests": 2, "domains": 3, - "notFound": 3, + "notFound": 2, "identicalFiles": 2, "emptyRequests": 3, "lazyLoadableImagesBelowTheFold": 2, From 1726e023ea65c6ca4311522dacb755a05b4efad8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sat, 2 Jan 2021 15:57:00 +0000 Subject: [PATCH 52/59] Rewording --- lib/metadata/policies.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 3a3591f..5ea155b 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -930,7 +930,7 @@ var policies = { "unit": 'bytes' }, "oldHttpProtocol": { - "label": "HTTP/1 requests", + "label": "HTTP protocols", "message": "

HTTP/2 is the latest version of the HTTP protocol. It is designed to optimize load speed. HTTP/3 will come soon and should be even faster!

When a domain sends more than 4 requests over HTTP/1, this metric counts one point for each new request. Below 5 requests, the benefits of HTTP/2 are generally less significant.

", "hasOffenders": true, "scoreFn": function(data) { @@ -971,7 +971,7 @@ var policies = { }, "oldTlsProtocol": { "tool": "phantomas", - "label": "Old TLS protocols", + "label": "TLS protocols", "message": "

Counts the number of domains that use TLS versions < 1.3. This is the latest version and it includes a faster \"handshake\" technology.

The 1.0 and 1.1 versions are deprecated because they are unsafe, switch to 1.2 or above as soon as possible.

The version 1.3 includes an even faster option called 0-RTT, check your server compatibility on SSL Labs.

", "isOkThreshold": 0, "isBadThreshold": 5, From 88483af607dd7d9107dafcf36e999041b4ee42cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sat, 2 Jan 2021 17:14:51 +0000 Subject: [PATCH 53/59] Detect WordPress for optional sponsoring message --- front/src/views/dashboard.html | 4 ++++ lib/runner.js | 4 ++++ lib/tools/redownload/redownload.js | 13 +++++++++++++ 3 files changed, 21 insertions(+) diff --git a/front/src/views/dashboard.html b/front/src/views/dashboard.html index e38ce19..808019d 100644 --- a/front/src/views/dashboard.html +++ b/front/src/views/dashboard.html @@ -55,6 +55,10 @@
+ <%if (sponsoring.wordpress) { %> + + <% } %> + <%if (sponsoring.dashboard) { %> <% } %> diff --git a/lib/runner.js b/lib/runner.js index 31e2044..29332fb 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -77,6 +77,10 @@ var Runner = function(params) { data.blockedRequests = data.toolsResults.phantomas.offenders.blockedRequests; } + // Report WordPress detection + if (data.toolsResults.redownload.metrics.isWordPress === true) { + data.frameworks = {isWordPress: true}; + } // Finished! deferred.resolve(data); diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index f7169c3..cb1adce 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -178,6 +178,8 @@ var Redownload = function() { offenders.unusedUnicodeRanges = listUnusedUnicodeRanges(results); metrics.unusedUnicodeRanges = offenders.unusedUnicodeRanges.count; + // Detect WordPress + metrics.isWordPress = detectWordPress(results); data.toolsResults.redownload = { metrics: metrics, @@ -741,6 +743,17 @@ var Redownload = function() { }; } + function detectWordPress(requests) { + // Check the first file only + if (requests[0].isHTML + && requests[0].weightCheck.bodyBuffer + && requests[0].weightCheck.bodyBuffer.indexOf('/wp-content/') > 0) { + + return true; + } + return false; + } + function redownloadEntry(entry, httpAuth, proxy) { var deferred = Q.defer(); From 64733d55f8e3fe02a9c3dc8895dce8befc027de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sat, 2 Jan 2021 22:24:42 +0000 Subject: [PATCH 54/59] Wording --- front/src/views/rule.html | 2 +- lib/metadata/policies.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/front/src/views/rule.html b/front/src/views/rule.html index 98b3bb3..8598ec6 100644 --- a/front/src/views/rule.html +++ b/front/src/views/rule.html @@ -488,7 +488,7 @@
File
Current weight
-
Woff 2 weight
+
WOFF 2 weight
Gain
diff --git a/lib/metadata/policies.js b/lib/metadata/policies.js index 5ea155b..c4583b4 100644 --- a/lib/metadata/policies.js +++ b/lib/metadata/policies.js @@ -897,7 +897,7 @@ var policies = { "heavyFonts": { "tool": "redownload", "label": "Overweighted webfonts", - "message": "

This metric is the sum of all bytes above 40KB in loaded fonts. Over this size, the font is probably not optimized for the web.

It can be a compresson issue, a font that contains too many glyphs or a font with complex shapes.

Sorry, Yellow Lab Tools is not yet compatible with the WOFF2 font format that generates 20-30% smaller fonts. You can proceed to a manual verification on a modern browser.

", + "message": "

This metric is the sum of all bytes above 40KB in loaded fonts. Over this size, the font is probably not optimized for the web.

It can be a compresson issue, a font that contains too many glyphs or a font with complex shapes.

", "isOkThreshold": 0, "isBadThreshold": 102400, "isAbnormalThreshold": 204800, @@ -921,8 +921,8 @@ var policies = { },*/ "nonWoff2Fonts": { "tool": "redownload", - "label": "Woff 2", - "message": "

The fonts listed here could be lighter if they were served with the latest woff 2 font file format. Some online tools can help you easily convert older formats to woff 2.

", + "label": "WOFF 2", + "message": "

The fonts listed here could be lighter if they were served with the latest WOFF 2 font file format. Some online tools can help you easily convert older formats to WOFF 2.

", "isOkThreshold": 0, "isBadThreshold": 51200, "isAbnormalThreshold": 122880, From bd9cb2e25783b13572ed853be5c8d6b0d56cf473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 3 Jan 2021 01:43:46 +0000 Subject: [PATCH 55/59] Add a progress bar on the UI --- front/src/css/queue.css | 13 +++++++++ front/src/js/controllers/queueCtrl.js | 11 ++++++-- front/src/less/queue.less | 15 ++++++++++ front/src/views/queue.html | 11 +++++++- lib/index.js | 4 +++ lib/runner.js | 37 ++++++++++++++++++++++++- lib/server/controllers/apiController.js | 10 +++++-- lib/server/datastores/runsDatastore.js | 10 +++++++ lib/tools/phantomas/phantomasWrapper.js | 6 ++++ lib/tools/redownload/redownload.js | 15 ++++++++-- 10 files changed, 122 insertions(+), 10 deletions(-) diff --git a/front/src/css/queue.css b/front/src/css/queue.css index 094ee24..64237d5 100644 --- a/front/src/css/queue.css +++ b/front/src/css/queue.css @@ -6,3 +6,16 @@ font-size: 0.8em; margin-bottom: 6em; } +.progressBarEmpty { + width: 90%; + max-width: 300px; + margin: 1em auto; + padding: 0.05em; + border: 1px solid #ffa319; +} +.progressBarFilled { + width: 5%; + height: 0.5em; + background: #ffa319; + transition: width 2s ease-out; +} diff --git a/front/src/js/controllers/queueCtrl.js b/front/src/js/controllers/queueCtrl.js index 17a3820..f62775d 100644 --- a/front/src/js/controllers/queueCtrl.js +++ b/front/src/js/controllers/queueCtrl.js @@ -9,15 +9,22 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs' Runs.get({runId: $scope.runId}, function(data) { $scope.url = data.params.url; $scope.status = data.status; + $scope.progress = data.progress; $scope.notFound = false; $scope.connectionLost = false; - if (data.status.statusCode === 'running' || data.status.statusCode === 'awaiting') { + if (data.status.statusCode === 'awaiting') { numberOfTries ++; - // Retrying in 2 seconds (and increasing the delay a bit more each time) + // Retrying every 2 seconds (and increasing the delay a bit more each time) setTimeout(getRunStatus, 2000 + (numberOfTries * 100)); + } else if (data.status.statusCode === 'running') { + numberOfTries ++; + + // Retrying every second or so + setTimeout(getRunStatus, 1000 + (numberOfTries * 10)); + } else if (data.status.statusCode === 'complete') { $location.path('/result/' + $scope.runId).replace(); } else { diff --git a/front/src/less/queue.less b/front/src/less/queue.less index f8d2cb2..41212c5 100644 --- a/front/src/less/queue.less +++ b/front/src/less/queue.less @@ -6,4 +6,19 @@ .statusSubMessage { font-size: 0.8em; margin-bottom: 6em; +} + +.progressBarEmpty { + width: 90%; + max-width: 300px; + margin: 1em auto; + padding: 0.05em; + border: 1px solid #ffa319; +} + +.progressBarFilled { + width: 5%; + height: 0.5em; + background: #ffa319; + transition: width 2s ease-out; } \ No newline at end of file diff --git a/front/src/views/queue.html b/front/src/views/queue.html index 204b332..e86b249 100644 --- a/front/src/views/queue.html +++ b/front/src/views/queue.html @@ -17,7 +17,16 @@
Test is running...
-

(auto-refresh activated)

+
+
+
+
+
+

(Phantomas launched)

+

(DOM Ready fired)

+

(page loaded, waiting for late requests)

+

(now simulating compression, optimization and minification)

+

(calculating score and retreaving screenshot)

Test complete
diff --git a/lib/index.js b/lib/index.js index 81819f4..9064046 100644 --- a/lib/index.js +++ b/lib/index.js @@ -24,9 +24,13 @@ var yellowLabTools = function(url, options) { }; var runner = new Runner(params) + + .progress(deferred.notify) + .then(function(data) { deferred.resolve(data); }) + .fail(function(err) { deferred.reject(err); }); diff --git a/lib/runner.js b/lib/runner.js index 29332fb..33389e5 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -24,7 +24,29 @@ var Runner = function(params) { // Execute Phantomas first phantomasWrapper.execute(data) + // For the progress bar + .progress(function(event) { + if (event === 'domReady') { + deferred.notify({ + estimatedProgress: 0.15, + milestone: 'domReady' + }); + } + if (event === 'domComplete') { + deferred.notify({ + estimatedProgress: 0.25, + milestone: 'domComplete' + }); + } + }) + .then(function(phantomasResults) { + // For the progress bar + deferred.notify({ + estimatedProgress: 0.4, + milestone: 'phantomas' + }); + data.toolsResults.phantomas = phantomasResults; // Mix all DOM Access metrics together @@ -37,12 +59,25 @@ var Runner = function(params) { data = mediaQueriesChecker.analyzeMediaQueries(data); // Redownload every file - return redownload.recheckAllFiles(data); + return redownload.recheckAllFiles(data) + + .progress(function(redownloadedProgress) { + deferred.notify({ + estimatedProgress: 0.4 + redownloadedProgress * 0.55, + milestone: 'phantomas' + }); + }); }) .then(function(data) { + // For the progress bar + deferred.notify({ + estimatedProgress: 0.99, + milestone: 'redownload' + }); + // Rules checker var policies = require('./metadata/policies'); data.rules = rulesChecker.check(data, policies); diff --git a/lib/server/controllers/apiController.js b/lib/server/controllers/apiController.js index d6f701f..3e057ec 100644 --- a/lib/server/controllers/apiController.js +++ b/lib/server/controllers/apiController.js @@ -84,11 +84,15 @@ var ApiController = function(app) { authPass: run.params.authPass, blockDomain: run.params.blockDomain, allowDomain: run.params.allowDomain, - noExternals: run.params.noExternals, - phantomasEngine: serverSettings.phantomasEngine + noExternals: run.params.noExternals }; - return ylt(run.params.url, runOptions); + return ylt(run.params.url, runOptions) + + // Update the progress bar on each progress + .progress(function(progress) { + runsDatastore.updateRunProgress(run.runId, progress); + }); }) diff --git a/lib/server/datastores/runsDatastore.js b/lib/server/datastores/runsDatastore.js index 8104b68..fd82fcc 100644 --- a/lib/server/datastores/runsDatastore.js +++ b/lib/server/datastores/runsDatastore.js @@ -42,6 +42,16 @@ function RunsDatastore() { }; + // When the test is launched, set the progress bar + this.updateRunProgress = function(runId, progress) { + var run = runs[runId]; + + run.progress = progress; + + runs[runId] = run; + }; + + this.markAsComplete = function(runId) { var run = runs[runId]; diff --git a/lib/tools/phantomas/phantomasWrapper.js b/lib/tools/phantomas/phantomasWrapper.js index 21af510..c75c648 100644 --- a/lib/tools/phantomas/phantomasWrapper.js +++ b/lib/tools/phantomas/phantomasWrapper.js @@ -79,6 +79,12 @@ var PhantomasWrapper = function() { deferred.reject('Phantomas failed: ' + res.message); }); + promise.on('milestone', function(event) { + if (event === 'domReady' || event === 'domComplete') { + deferred.notify(event); + } + }); + return deferred.promise; }; }; diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index cb1adce..f869b75 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -33,6 +33,7 @@ var Redownload = function() { function recheckAllFiles(data) { var startTime = Date.now(); debug('Redownload started'); + var deferred = Q.defer(); var requestsOffenders = data.toolsResults.phantomas.offenders.requests; @@ -43,6 +44,9 @@ var Redownload = function() { var requestsList = mergeOffenders(requestsOffenders, gzipOffenders, postOffenders, notFoundOffenders, redirectOffenders); + var totalCount = requestsList.length; + var doneCount = 0; + var httpAuth = null; if (data.params && data.params.options && data.params.options.authUser && data.params.options.authPass) { httpAuth = { @@ -89,6 +93,11 @@ var Redownload = function() { .then(function(newEntry) { debug('File %s - Redownloaded, optimized, minified, compressed, analyzed: done', entry.url); + + // For the progress bar + doneCount ++; + deferred.notify(doneCount/totalCount); + callback(null, newEntry); }) @@ -745,9 +754,9 @@ var Redownload = function() { function detectWordPress(requests) { // Check the first file only - if (requests[0].isHTML - && requests[0].weightCheck.bodyBuffer - && requests[0].weightCheck.bodyBuffer.indexOf('/wp-content/') > 0) { + if (requests[0].isHTML && + requests[0].weightCheck.bodyBuffer && + requests[0].weightCheck.bodyBuffer.indexOf('/wp-content/') > 0) { return true; } From 4b80310c0d3c0d58f6647376611258b9ffd772b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 3 Jan 2021 01:49:11 +0000 Subject: [PATCH 56/59] Fix settings files --- server_config/settings-dareboost.json | 19 ------------------- server_config/settings-prod.json | 11 +++++------ server_config/settings.json | 1 - 3 files changed, 5 insertions(+), 26 deletions(-) delete mode 100644 server_config/settings-dareboost.json diff --git a/server_config/settings-dareboost.json b/server_config/settings-dareboost.json deleted file mode 100644 index c32d5a4..0000000 --- a/server_config/settings-dareboost.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "serverPort": 80, - "phantomasEngine": "webkit", - "googleAnalyticsId": "", - "screenshotWidth": 400, - "baseUrl": "/", - "authorizedKeys": { - - }, - "maxAnonymousRunsPerDay": 99999999, - "maxAnonymousCallsPerDay": 99999999, - "blockedUrls": [], - - "sponsoring" : { - "home": "Generously hosted by



and sponsored by Dareboost, the website speed test service
", "dashboard": null, - "dashboard": "
Test your website for free and get additional insights with our sponsor: dareboost.com
", - "about": "


This public instance of Yellow Lab Tools is generously hosted by



Yellow Lab Tools is also made possible thanks to our sponsor
" - } -} \ No newline at end of file diff --git a/server_config/settings-prod.json b/server_config/settings-prod.json index a6efafd..1390bbb 100644 --- a/server_config/settings-prod.json +++ b/server_config/settings-prod.json @@ -1,6 +1,5 @@ { "serverPort": 80, - "phantomasEngine": "webkit", "googleAnalyticsId": "", "screenshotWidth": { "phone": 360, @@ -12,13 +11,13 @@ "authorizedKeys": { }, - "maxAnonymousRunsPerDay": 99999999, - "maxAnonymousCallsPerDay": 99999999, + "maxAnonymousRunsPerDay": 1000, + "maxAnonymousCallsPerDay": 100000, "blockedUrls": [], "sponsoring" : { - "home": "Generously hosted by



and sponsored by Dareboost, the website speed test service
", "dashboard": null, - "dashboard": "
Test your website for free and get additional insights with our sponsor: dareboost.com
", - "about": "


This public instance of Yellow Lab Tools is generously hosted by



Yellow Lab Tools is also made possible thanks to our sponsor
" + "home": "(this is a private instance)", + "dashboard": null, + "about": "(this is a private instance)" } } \ No newline at end of file diff --git a/server_config/settings.json b/server_config/settings.json index f8f5896..becee77 100644 --- a/server_config/settings.json +++ b/server_config/settings.json @@ -1,6 +1,5 @@ { "serverPort": 8383, - "phantomasEngine": "webkit", "googleAnalyticsId": "", "screenshotWidth": { "phone": 360, From 576dac3aff48dad5cbef509c887d5430af439ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 3 Jan 2021 19:06:55 +0000 Subject: [PATCH 57/59] Adding favicon animation and color codes --- front/src/img/favicon-fail.png | Bin 0 -> 764 bytes front/src/img/favicon-success.png | Bin 0 -> 719 bytes front/src/js/controllers/queueCtrl.js | 52 +++++++++++++++++++++++++- front/src/views/queue.html | 3 +- 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 front/src/img/favicon-fail.png create mode 100644 front/src/img/favicon-success.png diff --git a/front/src/img/favicon-fail.png b/front/src/img/favicon-fail.png new file mode 100644 index 0000000000000000000000000000000000000000..faeba8fabca5647c48d4982d97f99e10852b4f8c GIT binary patch literal 764 zcmVA$t%sF#r&Wu_n6&aC1C?V9sA~h0-!_Oa~i8QKkL|fGjnj9?wGEc_0t6NCb_R zsgJ(^m~dE<4S4_L)gO(G#ipE~0dyPucBuv!m^1Xa%xkCwSa3BWSEJ(6zK%wwEE zf<9T2h5I<~#qQE%r({Y-q8MO6t7K0QA13%j!xNSA_`Av-qLUa`nTVDm6u0Y@OrInX zQEyP}aSg1fl;*%YHD8OpSCc&wyFqQyOG(p)-Lne|x+Qg;AGVgw3LhoKIK%3~_8h=MPfMaj-NjkYYpbRXNoszBngg^+_BiostOGWBig>f+ ziUAH>Bvz-DL+USq^_o3K!Y%@INP4TLs+Qj#~g| zDBrCXA#2{Sn*qEb_zwYbj4yvmcm{Br=#*3(e+$wy@==113FntHoN+a}WX%PMb>KFT uvldlrt^~#~pAzoHnbMiYRM0AxLcakt!t_J!(XvVa0000#srKQk<6s+{& z1G*~}TiY(IlX+-noJ{WNqBE4}B%wO)`jJiU$^Cx!o|~K#8p`Hzm5POj`5HMn^0a`- zb(lL$M|^QWV_Y2GqaXufT)(e+UW5T;I(;)Ne2n-HG&AI-ky>MGlY6#V-$VTt>3ire zD07Rc7PiEQ9ndefUb1tWPKPM5uC(tAz`a4Gd8z&6o>wY-Isg&*#3l6~&<%hjHtEc( zN(?p$lDYtBj!=CJ=2yfjt&vy-=056UXpUSS5U1j@V2R`tMb*|WsLH7|#RA1;fp`QX z0^rODXB3HD81H;J(%*C9Bb-q+J4V6i_Oy%HW>2T(Of*^ZQ!0Mc+&HyJboMH6tvau0S&f+^NxxvmboP1}i4x?&->^YLB zNIq0>!)iSMc<6iV#j0e_ks9c*iF5rxqI_4Lj8XCpjJJw4;&{Y`21E?i2@(bwTMYk_ z&h`U$R8*!2fISU+T1G72ugVlz>q7hF%c?&`Iu%z%PzR2br=hqip!*W#130t7$zIUC z<&cFl>+}43qXy_`KDu~C>S*3~+HC-#yd;~6%lo65KzT``jc5V9L2;bxg8LsG=im(r zAD}itO{;5wriI!a@&Ua5uDQ1iQ}6-epAq=~_U|Hw)-J#;fbl^Y2VDc0`!F}#?QbZX zZw*^ios;z(PSa2q`qqRCy$F39z0W-=od;ZvegkE&@gu?E0p|b!002ovPDHLkV1lW! BP^bU^ literal 0 HcmV?d00001 diff --git a/front/src/js/controllers/queueCtrl.js b/front/src/js/controllers/queueCtrl.js index f62775d..4c61d2d 100644 --- a/front/src/js/controllers/queueCtrl.js +++ b/front/src/js/controllers/queueCtrl.js @@ -5,6 +5,16 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs' var numberOfTries = 0; + var favicon = document.querySelector('link[rel=icon]'); + var faviconUrl = 'img/favicon.png'; + var faviconSuccessUrl = 'img/favicon-success.png'; + var faviconFailUrl = 'img/favicon-fail.png'; + var faviconInterval = null; + var faviconCounter = 0; + var faviconCanvas = null; + var faviconCanvasContext = null; + var faviconImage = null; + function getRunStatus () { Runs.get({runId: $scope.runId}, function(data) { $scope.url = data.params.url; @@ -15,23 +25,30 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs' if (data.status.statusCode === 'awaiting') { numberOfTries ++; + rotateFavicon(); // Retrying every 2 seconds (and increasing the delay a bit more each time) setTimeout(getRunStatus, 2000 + (numberOfTries * 100)); } else if (data.status.statusCode === 'running') { numberOfTries ++; + rotateFavicon(); // Retrying every second or so setTimeout(getRunStatus, 1000 + (numberOfTries * 10)); } else if (data.status.statusCode === 'complete') { + stopFavicon(true); + $location.path('/result/' + $scope.runId).replace(); } else { - // Handled by the view + stopFavicon(false); + + // The rest is handled by the view } }, function(response) { if (response.status === 404) { + stopFavicon(false); $scope.notFound = true; $scope.connectionLost = false; } else if (response.status === 0) { @@ -42,6 +59,39 @@ queueCtrl.controller('QueueCtrl', ['$scope', '$routeParams', '$location', 'Runs' } }); } + + function rotateFavicon() { + if (!faviconInterval) { + faviconImage = new Image(); + faviconImage.onload = function() { + faviconCanvas = document.getElementById('faviconRotator'); + faviconCanvasContext = faviconCanvas.getContext('2d'); + faviconCanvasContext.fillStyle = '#212240'; + + if (!!faviconCanvasContext) { + faviconInterval = window.setInterval(faviconTick, 1000); + } + }; + faviconImage.src = faviconUrl; + } + } + + function faviconTick() { + faviconCounter ++; + faviconCanvasContext.save(); + faviconCanvasContext.fillRect(0, 0, 32, 32); + faviconCanvasContext.translate(16, 16); + faviconCanvasContext.rotate(45 * faviconCounter * Math.PI / 180); + faviconCanvasContext.translate(-16, -16); + faviconCanvasContext.drawImage(faviconImage, 0, 0, 32, 32); + faviconCanvasContext.restore(); + favicon.href = faviconCanvas.toDataURL('image/png'); + } + + function stopFavicon(isSuccess) { + window.clearInterval(faviconInterval); + favicon.href = isSuccess ? faviconSuccessUrl : faviconFailUrl; + } getRunStatus(); }]); diff --git a/front/src/views/queue.html b/front/src/views/queue.html index e86b249..d628768 100644 --- a/front/src/views/queue.html +++ b/front/src/views/queue.html @@ -42,4 +42,5 @@
Connection lost with server

Check your wifi cable, or maybe YellowLab.tools is rebooting.

-
\ No newline at end of file +
+ \ No newline at end of file From 294c5ab06de026e99ae7244eb072734add1b60fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Sun, 3 Jan 2021 21:44:47 +0000 Subject: [PATCH 58/59] Adding favicon animation and color codes --- front/src/css/queue.css | 2 +- front/src/img/favicon-fail.png | Bin 764 -> 706 bytes front/src/img/favicon-success.png | Bin 719 -> 551 bytes front/src/js/controllers/queueCtrl.js | 5 +++-- front/src/js/services/settingsService.js | 2 +- front/src/less/queue.less | 2 +- front/src/views/index.html | 2 +- front/src/views/queue.html | 2 +- 8 files changed, 8 insertions(+), 7 deletions(-) diff --git a/front/src/css/queue.css b/front/src/css/queue.css index 64237d5..e6b9fa2 100644 --- a/front/src/css/queue.css +++ b/front/src/css/queue.css @@ -17,5 +17,5 @@ width: 5%; height: 0.5em; background: #ffa319; - transition: width 2s ease-out; + transition: width 3s ease-out; } diff --git a/front/src/img/favicon-fail.png b/front/src/img/favicon-fail.png index faeba8fabca5647c48d4982d97f99e10852b4f8c..8192c94422bb4873fcf8c667ea4b03ef879ee198 100644 GIT binary patch delta 682 zcmV;b0#*I|1;Pc8B!7uXL_t(|0qoU-vm7xT2k`zY&F(o_ow51lSlb%Zwyi;JyB*Z# zsJ3m}Hnw>-_d4v%_q|g#pEy;jBKA*H2GGXF7GYTB(u>V>5{rq{6tGN={UC~wNEDlQ zvag#nW(ue&(m@V*PgEljALV2toxrmWM|ka*h`+Cc1q5?F;D14gJ(2xJA}Qp2?CfA1 z*oni)Nk~Fq z7OjrJYOXEn$LYFp?%Iq-~ldOMhC5@=|Jd$Ru{QCr*czH}m(;bNbOvQMG9nVqa4 zx;(L25*2U5RD}wQ^s6j~Pg%!_CvK+}OnfNWqO>LzDNfDmN0baDyxgv)Y0g4A~ z2>7h>LVu=$@oWrYu|PXJ`%VrD$_ zM59k7Zwng~0FJ1X|CX+N$MOoPU=OZn=>&cgX`>u10Bpi~8i7wlnd~aq16_pg?m*Ul zF`H2EqDEB#3NKTS06THyu@?TM1-_}iipTp=3nSdp^yT>PKt%{jGNma30P>e}9MdqH QmH+?%07*qoM6N<$g8Y6uq5uE@ delta 740 zcmV7(UPaXFkTY+>T9F|pnpT$a~i8QKkL|fGjnj9?wGEc_0t6NCb_RsgJ(^m~dE<4S4_L)g1-zmFNeZE2tFGub}}6?I7KW8}faq zvH%0x#oeW;&tmoAqzi-njld(Eq{<;lte2cmB9sNJBX@x0a`EmG8P!0MlR0>$!RHcg z*33TAHF$pqu+g33wrjed@PLNWrE+x=;{ncRv5peoD1W(fo|fKkA=^zNAx57%;}pA} zm%^}G4~Ya-&k~Q8xkL%TGQ2&KZq>|VoI!#U_h&6PY@p_ z_(a1KmGbzz${nJU7+0BymLe3l>y=EOBoR?>Q0#FHtf-Xcz&kZxi@jHqJrcV?ZP80f z(}vx%3x5l`C3T%2wwBEbA0@>&!|JzM;B>(YtR;7z*y5}Mj(S&^tSyZ89Kb?POQJ>H z#aYj5tELV~YJP*71GGu@IPq$%12%e!c(de+0S;UwR;QIi>Mw!ynmtCsE&_B&daI_Y zw4PT8Z#$_?k|_XmlQb7FJr*K?)jHjdTL5S%-(9U1A#2{Sn*qEb_zwYbj4yvmcm{Br z=#*3(e+$wy@==113FntHoN+a}WX%PMb>KFTvldlrt^~#~pAzoHnbMiYRM0AxLcakt W!t_J!(XvVa0000s}u05ji!_hz&gJt}IoNVX*JI2OVd7*(W1}nR*0DsU9=*q0uaUvxEx){V% zJpkg9xb_=ao=}JWRd#4t9uo#JRW_gpv)(4nPYTbIydrbB<_BR4g0cX?)&Fm@<64?g zcjmZAn1Y~mK!+jB@Lth=p#)|4%;Y$x`$P~xRuZ7g1lLKzY4PsM`i=hW|Njp%h>Hfq zrg6=8GQOY=1AnuBU>|0FMvy^VB%m*|-oepS!VB{(adZ~f{8S)DND+YGI-&kej+=zd z%y&yBxK9(r2q_eh9K{T;N%w`~3P(3@N{(UrcM3ufSO5TDbo~Fr;66^t0W8_CfYhJ) zc2|Z7gy#XoCUfl%g1Ur+K!oQ6^kkNYkd~eh4wI~a;C}`IQJD!QnE}`$-A5`P025fH z0Xk1`oxybdBv`>v0wg%seX2JAtY9bs{96Sk>M{9u2npahT+aX)N`TN3q5e~E?yw3j z;}W350KAL!2CzJ)4*gi#J?M)YPqs&-e@og=r6dH}9+G!Abr>Wc(*{ld0LYWll_pSh RUH||900>D%PDHLkV1iXA`O5$R delta 696 zcmV;p0!RI)1kVMKBYy&xNkl#srKQk<6s+{&1G*~}TiY(IlX+-noJ{WNqBE4}B%wO)`jJiU z$^Cx!o|~K#8p`Hzm5POj`5HMn^0a`-b(lL$M|^QWV_Y2GqkkX+VqCwkdR~M9WIBB_ zEPRak4>U96rIA`=YmHqb7U_HFFDP@1sTQ`xh#k-`wqCMxn@)!)v97f548XlX zrFp6S*DSwus5at&(@)v$_x#EWeQB?;D2K2aOQ!6AbU~3>YS*~04Fb; zVf5~@u@O@~QSuFpw~965c*KPUL=4pl5(XJt4F8hO_5*iRRHg`kJq>$WMl9d2$`o1aLi^;) zsy{_K6@OPnPzR2br=hqip!*W#130t7$zIUC<&cFl>+}43qXy_`KDu~C>S*3~+HC-# zyd;~6%lo65KzT``jc5V9L2;bxg8LsG=im(rAD}itO{;5wriI!a@&Ua5uDQ1iQ}6-e zpAq=~_U|Hw)-J#;fbl^Y2VDc0`!F}#?QbZXZ!ry9RGpLc98S|v7y8zO3%v+^8@Free and open source!

- +
diff --git a/front/src/views/queue.html b/front/src/views/queue.html index d628768..3910373 100644 --- a/front/src/views/queue.html +++ b/front/src/views/queue.html @@ -26,7 +26,7 @@

(DOM Ready fired)

(page loaded, waiting for late requests)

(now simulating compression, optimization and minification)

-

(calculating score and retreaving screenshot)

+

(calculating score and retrieving screenshot)

Test complete
From fd11e8c1fec3909b42acf238fde43fd318083011 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 5 Jan 2021 22:19:02 +0000 Subject: [PATCH 59/59] In some rare cases, the HTML minifier can be very slow, timeouting other simultaneous requests --- lib/tools/redownload/redownload.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/tools/redownload/redownload.js b/lib/tools/redownload/redownload.js index f869b75..81270cc 100644 --- a/lib/tools/redownload/redownload.js +++ b/lib/tools/redownload/redownload.js @@ -25,8 +25,8 @@ var imageDimensions = require('./imageDimensions'); var Redownload = function() { - var MAX_PARALLEL_DOWNLOADS = 10; - var REQUEST_TIMEOUT = 15000; // 15 seconds + var MAX_PARALLEL_DOWNLOADS = 5; + var REQUEST_TIMEOUT = 30000; // 30 seconds // This function will re-download every asset and check if it could be optimized