diff --git a/src/display/font_loader.js b/src/display/font_loader.js index bed322135..6525e99d0 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -14,125 +14,86 @@ */ import { - assert, bytesToString, isEvalSupported, shadow, string32, + assert, bytesToString, isEvalSupported, shadow, string32, unreachable, UNSUPPORTED_FEATURES, warn } from '../shared/util'; -function FontLoader(docId) { - this.docId = docId; - this.styleElement = null; - if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { +class BaseFontLoader { + constructor(docId) { + if (this.constructor === BaseFontLoader) { + unreachable('Cannot initialize BaseFontLoader.'); + } + this.docId = docId; + this.nativeFontFaces = []; - this.loadTestFontId = 0; + this.styleElement = null; this.loadingContext = { requests: [], nextRequestId: 0, }; } -} -FontLoader.prototype = { - insertRule: function fontLoaderInsertRule(rule) { - var styleElement = this.styleElement; + + addNativeFontFace(nativeFontFace) { + this.nativeFontFaces.push(nativeFontFace); + document.fonts.add(nativeFontFace); + } + + insertRule(rule) { + let styleElement = this.styleElement; if (!styleElement) { styleElement = this.styleElement = document.createElement('style'); - styleElement.id = 'PDFJS_FONT_STYLE_TAG_' + this.docId; + styleElement.id = `PDFJS_FONT_STYLE_TAG_${this.docId}`; document.documentElement.getElementsByTagName('head')[0].appendChild( styleElement); } - var styleSheet = styleElement.sheet; + const styleSheet = styleElement.sheet; styleSheet.insertRule(rule, styleSheet.cssRules.length); - }, + } + + clear() { + this.nativeFontFaces.forEach(function(nativeFontFace) { + document.fonts.delete(nativeFontFace); + }); + this.nativeFontFaces.length = 0; - clear: function fontLoaderClear() { if (this.styleElement) { // Note: ChildNode.remove doesn't throw if the parentNode is undefined. this.styleElement.remove(); this.styleElement = null; } - if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { - this.nativeFontFaces.forEach(function(nativeFontFace) { - document.fonts.delete(nativeFontFace); - }); - this.nativeFontFaces.length = 0; - } - }, -}; + } -if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { - var getLoadTestFont = function () { - // This is a CFF font with 1 glyph for '.' that fills its entire width and - // height. - return atob( - 'T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQAFQ' + - 'AABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAAALwA' + - 'AAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgAAAAGbm' + - 'FtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1AAsD6AAA' + - 'AADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD6AAAAAAD6A' + - 'ABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACMAooCvAAAAeAA' + - 'MQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4DIP84AFoDIQAAAA' + - 'AAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAAAAEAAQAAAAEAAAAA' + - 'AAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUAAQAAAAEAAAAAAAYAAQ' + - 'AAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgABAAMAAQQJAAMAAgABAAMA' + - 'AQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABYAAAAAAAAAwAAAAMAAAAcAA' + - 'EAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAAAC7////TAAEAAAAAAAABBgAA' + - 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAA' + - 'AAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAAAAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgc' + - 'A/gXBIwMAYuL+nz5tQXkD5j3CBLnEQACAQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWF' + - 'hYWFhYWFhYAAABAQAADwACAQEEE/t3Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQA' + - 'AAAAAAABAAAAAMmJbzEAAAAAzgTjFQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAg' + - 'ABAAAAAAAAAAAD6AAAAAAAAA=='); - }; - Object.defineProperty(FontLoader.prototype, 'loadTestFont', { - get() { - return shadow(this, 'loadTestFont', getLoadTestFont()); - }, - configurable: true, - }); - - FontLoader.prototype.addNativeFontFace = - function fontLoader_addNativeFontFace(nativeFontFace) { - this.nativeFontFaces.push(nativeFontFace); - document.fonts.add(nativeFontFace); - }; - - FontLoader.prototype.bind = function fontLoaderBind(fonts, callback) { - var rules = []; - var fontsToLoad = []; - var fontLoadPromises = []; - var getNativeFontPromise = function(nativeFontFace) { + bind(fonts, callback) { + const rules = []; + const fontsToLoad = []; + const fontLoadPromises = []; + const getNativeFontPromise = function(nativeFontFace) { // Return a promise that is always fulfilled, even when the font fails to // load. - return nativeFontFace.loaded.catch(function(e) { - warn('Failed to load font "' + nativeFontFace.family + '": ' + e); + return nativeFontFace.loaded.catch(function(reason) { + warn(`Failed to load font "${nativeFontFace.family}": ${reason}`); }); }; // Firefox Font Loading API does not work with mozPrintCallback -- // disabling it in this case. - var isFontLoadingAPISupported = FontLoader.isFontLoadingAPISupported && - !FontLoader.isSyncFontLoadingSupported; - for (var i = 0, ii = fonts.length; i < ii; i++) { - var font = fonts[i]; - - // Add the font to the DOM only once or skip if the font - // is already loaded. + const isFontLoadingAPISupported = this.isFontLoadingAPISupported && + !this.isSyncFontLoadingSupported; + for (const font of fonts) { + // Add the font to the DOM only once; skip if the font is already loaded. if (font.attached || font.missingFile) { continue; } font.attached = true; if (isFontLoadingAPISupported) { - var nativeFontFace = font.createNativeFontFace(); + const nativeFontFace = font.createNativeFontFace(); if (nativeFontFace) { this.addNativeFontFace(nativeFontFace); fontLoadPromises.push(getNativeFontPromise(nativeFontFace)); } } else { - var rule = font.createFontFaceRule(); + const rule = font.createFontFaceRule(); if (rule) { this.insertRule(rule); rules.push(rule); @@ -141,194 +102,231 @@ if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { } } - var request = this.queueLoadingCallback(callback); + const request = this._queueLoadingCallback(callback); if (isFontLoadingAPISupported) { - Promise.all(fontLoadPromises).then(function() { - request.complete(); - }); - } else if (rules.length > 0 && !FontLoader.isSyncFontLoadingSupported) { - this.prepareFontLoadEvent(rules, fontsToLoad, request); + Promise.all(fontLoadPromises).then(request.complete); + } else if (rules.length > 0 && !this.isSyncFontLoadingSupported) { + this._prepareFontLoadEvent(rules, fontsToLoad, request); } else { request.complete(); } - }; + } - FontLoader.prototype.queueLoadingCallback = - function FontLoader_queueLoadingCallback(callback) { - function LoadLoader_completeRequest() { + _queueLoadingCallback(callback) { + function completeRequest() { assert(!request.done, 'completeRequest() cannot be called twice.'); request.done = true; // Sending all completed requests in order of how they were queued. while (context.requests.length > 0 && context.requests[0].done) { - var otherRequest = context.requests.shift(); + const otherRequest = context.requests.shift(); setTimeout(otherRequest.callback, 0); } } - var context = this.loadingContext; - var requestId = 'pdfjs-font-loading-' + (context.nextRequestId++); - var request = { - id: requestId, + const context = this.loadingContext; + const request = { + id: `pdfjs-font-loading-${context.nextRequestId++}`, done: false, - complete: LoadLoader_completeRequest, + complete: completeRequest, callback, }; context.requests.push(request); return request; - }; + } - FontLoader.prototype.prepareFontLoadEvent = - function fontLoaderPrepareFontLoadEvent(rules, fonts, request) { - /** Hack begin */ - // There's currently no event when a font has finished downloading so the - // following code is a dirty hack to 'guess' when a font is - // ready. It's assumed fonts are loaded in order, so add a known test - // font after the desired fonts and then test for the loading of that - // test font. + get isFontLoadingAPISupported() { + unreachable('Abstract method `isFontLoadingAPISupported`.'); + } - function int32(data, offset) { - return (data.charCodeAt(offset) << 24) | - (data.charCodeAt(offset + 1) << 16) | - (data.charCodeAt(offset + 2) << 8) | - (data.charCodeAt(offset + 3) & 0xff); - } + get isSyncFontLoadingSupported() { + unreachable('Abstract method `isSyncFontLoadingSupported`.'); + } - function spliceString(s, offset, remove, insert) { - var chunk1 = s.substring(0, offset); - var chunk2 = s.substring(offset + remove); - return chunk1 + insert + chunk2; - } + get _loadTestFont() { + unreachable('Abstract method `_loadTestFont`.'); + } - var i, ii; + _prepareFontLoadEvent(rules, fontsToLoad, request) { + unreachable('Abstract method `_prepareFontLoadEvent`.'); + } +} - // The temporary canvas is used to determine if fonts are loaded. - var canvas = document.createElement('canvas'); - canvas.width = 1; - canvas.height = 1; - var ctx = canvas.getContext('2d'); +let FontLoader; +if (typeof PDFJSDev !== 'undefined' && PDFJSDev.test('MOZCENTRAL')) { - var called = 0; - function isFontReady(name, callback) { - called++; - // With setTimeout clamping this gives the font ~100ms to load. - if (called > 30) { - warn('Load test font never loaded.'); - callback(); - return; +FontLoader = class MozcentralFontLoader extends BaseFontLoader { + get isFontLoadingAPISupported() { + return shadow(this, 'isFontLoadingAPISupported', + typeof document !== 'undefined' && !!document.fonts); + } + + get isSyncFontLoadingSupported() { + return shadow(this, 'isSyncFontLoadingSupported', true); + } +}; + +} else { // PDFJSDev.test('CHROME || GENERIC') + +FontLoader = class GenericFontLoader extends BaseFontLoader { + constructor(docId) { + super(docId); + this.loadTestFontId = 0; + } + + get isFontLoadingAPISupported() { + let supported = (typeof document !== 'undefined' && !!document.fonts); + return shadow(this, 'isFontLoadingAPISupported', supported); + } + + get isSyncFontLoadingSupported() { + let supported = false; + if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('CHROME')) { + if (typeof navigator === 'undefined') { + // Node.js - we can pretend that sync font loading is supported. + supported = true; + } else { + // User agent string sniffing is bad, but there is no reliable way to + // tell if the font is fully loaded and ready to be used with canvas. + const m = /Mozilla\/5.0.*?rv:(\d+).*? Gecko/.exec(navigator.userAgent); + if (m && m[1] >= 14) { + supported = true; } - ctx.font = '30px ' + name; - ctx.fillText('.', 0, 20); - var imageData = ctx.getImageData(0, 0, 1, 1); - if (imageData.data[3] > 0) { - callback(); - return; - } - setTimeout(isFontReady.bind(null, name, callback)); - } - - var loadTestFontId = 'lt' + Date.now() + this.loadTestFontId++; - // Chromium seems to cache fonts based on a hash of the actual font data, - // so the font must be modified for each load test else it will appear to - // be loaded already. - // TODO: This could maybe be made faster by avoiding the btoa of the full - // font by splitting it in chunks before hand and padding the font id. - var data = this.loadTestFont; - var COMMENT_OFFSET = 976; // has to be on 4 byte boundary (for checksum) - data = spliceString(data, COMMENT_OFFSET, loadTestFontId.length, - loadTestFontId); - // CFF checksum is important for IE, adjusting it - var CFF_CHECKSUM_OFFSET = 16; - var XXXX_VALUE = 0x58585858; // the "comment" filled with 'X' - var checksum = int32(data, CFF_CHECKSUM_OFFSET); - for (i = 0, ii = loadTestFontId.length - 3; i < ii; i += 4) { - checksum = (checksum - XXXX_VALUE + int32(loadTestFontId, i)) | 0; - } - if (i < loadTestFontId.length) { // align to 4 bytes boundary - checksum = (checksum - XXXX_VALUE + - int32(loadTestFontId + 'XXX', i)) | 0; - } - data = spliceString(data, CFF_CHECKSUM_OFFSET, 4, string32(checksum)); - - var url = 'url(data:font/opentype;base64,' + btoa(data) + ');'; - var rule = '@font-face { font-family:"' + loadTestFontId + '";src:' + - url + '}'; - this.insertRule(rule); - - var names = []; - for (i = 0, ii = fonts.length; i < ii; i++) { - names.push(fonts[i].loadedName); - } - names.push(loadTestFontId); - - var div = document.createElement('div'); - div.setAttribute('style', - 'visibility: hidden;' + - 'width: 10px; height: 10px;' + - 'position: absolute; top: 0px; left: 0px;'); - for (i = 0, ii = names.length; i < ii; ++i) { - var span = document.createElement('span'); - span.textContent = 'Hi'; - span.style.fontFamily = names[i]; - div.appendChild(span); - } - document.body.appendChild(div); - - isFontReady(loadTestFontId, function() { - document.body.removeChild(div); - request.complete(); - }); - /** Hack end */ - }; -} else { - FontLoader.prototype.bind = function fontLoaderBind(fonts, callback) { - for (var i = 0, ii = fonts.length; i < ii; i++) { - var font = fonts[i]; - if (font.attached) { - continue; - } - - font.attached = true; - var rule = font.createFontFaceRule(); - if (rule) { - this.insertRule(rule); + // TODO - other browsers... } } + return shadow(this, 'isSyncFontLoadingSupported', supported); + } - setTimeout(callback); - }; -} -if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL')) { - FontLoader.isFontLoadingAPISupported = typeof document !== 'undefined' && - !!document.fonts; -} -if (typeof PDFJSDev === 'undefined' || !PDFJSDev.test('MOZCENTRAL || CHROME')) { - var isSyncFontLoadingSupported = function isSyncFontLoadingSupported() { - if (typeof navigator === 'undefined') { - // node.js - we can pretend sync font loading is supported. - return true; + get _loadTestFont() { + const getLoadTestFont = function() { + // This is a CFF font with 1 glyph for '.' that fills its entire width and + // height. + return atob( + 'T1RUTwALAIAAAwAwQ0ZGIDHtZg4AAAOYAAAAgUZGVE1lkzZwAAAEHAAAABxHREVGABQA' + + 'FQAABDgAAAAeT1MvMlYNYwkAAAEgAAAAYGNtYXABDQLUAAACNAAAAUJoZWFk/xVFDQAA' + + 'ALwAAAA2aGhlYQdkA+oAAAD0AAAAJGhtdHgD6AAAAAAEWAAAAAZtYXhwAAJQAAAAARgA' + + 'AAAGbmFtZVjmdH4AAAGAAAAAsXBvc3T/hgAzAAADeAAAACAAAQAAAAEAALZRFsRfDzz1' + + 'AAsD6AAAAADOBOTLAAAAAM4KHDwAAAAAA+gDIQAAAAgAAgAAAAAAAAABAAADIQAAAFoD' + + '6AAAAAAD6AABAAAAAAAAAAAAAAAAAAAAAQAAUAAAAgAAAAQD6AH0AAUAAAKKArwAAACM' + + 'AooCvAAAAeAAMQECAAACAAYJAAAAAAAAAAAAAQAAAAAAAAAAAAAAAFBmRWQAwAAuAC4D' + + 'IP84AFoDIQAAAAAAAQAAAAAAAAAAACAAIAABAAAADgCuAAEAAAAAAAAAAQAAAAEAAAAA' + + 'AAEAAQAAAAEAAAAAAAIAAQAAAAEAAAAAAAMAAQAAAAEAAAAAAAQAAQAAAAEAAAAAAAUA' + + 'AQAAAAEAAAAAAAYAAQAAAAMAAQQJAAAAAgABAAMAAQQJAAEAAgABAAMAAQQJAAIAAgAB' + + 'AAMAAQQJAAMAAgABAAMAAQQJAAQAAgABAAMAAQQJAAUAAgABAAMAAQQJAAYAAgABWABY' + + 'AAAAAAAAAwAAAAMAAAAcAAEAAAAAADwAAwABAAAAHAAEACAAAAAEAAQAAQAAAC7//wAA' + + 'AC7////TAAEAAAAAAAABBgAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMAAAAAAAD/gwAyAAAAAQAAAAAAAAAAAAAAAAAA' + + 'AAABAAQEAAEBAQJYAAEBASH4DwD4GwHEAvgcA/gXBIwMAYuL+nz5tQXkD5j3CBLnEQAC' + + 'AQEBIVhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYWFhYAAABAQAADwACAQEEE/t3' + + 'Dov6fAH6fAT+fPp8+nwHDosMCvm1Cvm1DAz6fBQAAAAAAAABAAAAAMmJbzEAAAAAzgTj' + + 'FQAAAADOBOQpAAEAAAAAAAAADAAUAAQAAAABAAAAAgABAAAAAAAAAAAD6AAAAAAAAA=='); + }; + return shadow(this, '_loadTestFont', getLoadTestFont()); + } + + _prepareFontLoadEvent(rules, fonts, request) { + /** Hack begin */ + // There's currently no event when a font has finished downloading so the + // following code is a dirty hack to 'guess' when a font is ready. + // It's assumed fonts are loaded in order, so add a known test font after + // the desired fonts and then test for the loading of that test font. + + function int32(data, offset) { + return (data.charCodeAt(offset) << 24) | + (data.charCodeAt(offset + 1) << 16) | + (data.charCodeAt(offset + 2) << 8) | + (data.charCodeAt(offset + 3) & 0xff); + } + function spliceString(s, offset, remove, insert) { + let chunk1 = s.substring(0, offset); + let chunk2 = s.substring(offset + remove); + return chunk1 + insert + chunk2; + } + let i, ii; + + // The temporary canvas is used to determine if fonts are loaded. + let canvas = document.createElement('canvas'); + canvas.width = 1; + canvas.height = 1; + let ctx = canvas.getContext('2d'); + + let called = 0; + function isFontReady(name, callback) { + called++; + // With setTimeout clamping this gives the font ~100ms to load. + if (called > 30) { + warn('Load test font never loaded.'); + callback(); + return; + } + ctx.font = '30px ' + name; + ctx.fillText('.', 0, 20); + let imageData = ctx.getImageData(0, 0, 1, 1); + if (imageData.data[3] > 0) { + callback(); + return; + } + setTimeout(isFontReady.bind(null, name, callback)); } - var supported = false; - - // User agent string sniffing is bad, but there is no reliable way to tell - // if font is fully loaded and ready to be used with canvas. - var m = /Mozilla\/5.0.*?rv:(\d+).*? Gecko/.exec(navigator.userAgent); - if (m && m[1] >= 14) { - supported = true; + const loadTestFontId = `lt${Date.now()}${this.loadTestFontId++}`; + // Chromium seems to cache fonts based on a hash of the actual font data, + // so the font must be modified for each load test else it will appear to + // be loaded already. + // TODO: This could maybe be made faster by avoiding the btoa of the full + // font by splitting it in chunks before hand and padding the font id. + let data = this._loadTestFont; + let COMMENT_OFFSET = 976; // has to be on 4 byte boundary (for checksum) + data = spliceString(data, COMMENT_OFFSET, loadTestFontId.length, + loadTestFontId); + // CFF checksum is important for IE, adjusting it + let CFF_CHECKSUM_OFFSET = 16; + let XXXX_VALUE = 0x58585858; // the "comment" filled with 'X' + let checksum = int32(data, CFF_CHECKSUM_OFFSET); + for (i = 0, ii = loadTestFontId.length - 3; i < ii; i += 4) { + checksum = (checksum - XXXX_VALUE + int32(loadTestFontId, i)) | 0; } - // TODO other browsers - return supported; - }; - Object.defineProperty(FontLoader, 'isSyncFontLoadingSupported', { - get() { - return shadow(FontLoader, 'isSyncFontLoadingSupported', - isSyncFontLoadingSupported()); - }, - enumerable: true, - configurable: true, - }); -} + if (i < loadTestFontId.length) { // align to 4 bytes boundary + checksum = (checksum - XXXX_VALUE + int32(loadTestFontId + 'XXX', i)) | 0; + } + data = spliceString(data, CFF_CHECKSUM_OFFSET, 4, string32(checksum)); + + const url = `url(data:font/opentype;base64,${btoa(data)});`; + const rule = `@font-face {font-family:"${loadTestFontId}";src:${url}}`; + this.insertRule(rule); + + let names = []; + for (i = 0, ii = fonts.length; i < ii; i++) { + names.push(fonts[i].loadedName); + } + names.push(loadTestFontId); + + let div = document.createElement('div'); + div.setAttribute('style', 'visibility: hidden;' + + 'width: 10px; height: 10px;' + + 'position: absolute; top: 0px; left: 0px;'); + for (i = 0, ii = names.length; i < ii; ++i) { + let span = document.createElement('span'); + span.textContent = 'Hi'; + span.style.fontFamily = names[i]; + div.appendChild(span); + } + document.body.appendChild(div); + + isFontReady(loadTestFontId, function() { + document.body.removeChild(div); + request.complete(); + }); + /** Hack end */ + } +}; + +} // End of PDFJSDev.test('CHROME || GENERIC') const IsEvalSupportedCached = { get value() {