From abd7084bcc3c75b49732fadfe1077010748655b8 Mon Sep 17 00:00:00 2001 From: Yury Semikhatsky Date: Fri, 11 Feb 2022 09:06:17 -0800 Subject: [PATCH] fix: match default font families in headless chromium (#11340) --- .../src/server/chromium/crBrowser.ts | 15 +- .../src/server/chromium/crPage.ts | 49 +++--- .../server/chromium/defaultFontFamilies.ts | 157 ++++++++++++++++++ tests/headful.spec.ts | 33 +++- ...generate_chromium_default_font_families.js | 117 +++++++++++++ 5 files changed, 347 insertions(+), 24 deletions(-) create mode 100644 packages/playwright-core/src/server/chromium/defaultFontFamilies.ts create mode 100644 utils/generate_chromium_default_font_families.js diff --git a/packages/playwright-core/src/server/chromium/crBrowser.ts b/packages/playwright-core/src/server/chromium/crBrowser.ts index c0373eb435..bc6e30ec21 100644 --- a/packages/playwright-core/src/server/chromium/crBrowser.ts +++ b/packages/playwright-core/src/server/chromium/crBrowser.ts @@ -40,7 +40,6 @@ export class CRBrowser extends Browser { _backgroundPages = new Map(); _serviceWorkers = new Map(); _devtools?: CRDevTools; - _isMac = false; private _version = ''; private _tracingRecording = false; @@ -49,6 +48,8 @@ export class CRBrowser extends Browser { private _userAgent: string = ''; static async connect(transport: ConnectionTransport, options: BrowserOptions, devtools?: CRDevTools): Promise { + // Make a copy in case we need to update `headful` property below. + options = { ...options }; const connection = new CRConnection(transport, options.protocolLogger, options.browserLogsCollector); const browser = new CRBrowser(connection, options); browser._devtools = devtools; @@ -57,9 +58,11 @@ export class CRBrowser extends Browser { await (options as any).__testHookOnConnectToBrowser(); const version = await session.send('Browser.getVersion'); - browser._isMac = version.userAgent.includes('Macintosh'); browser._version = version.product.substring(version.product.indexOf('/') + 1); browser._userAgent = version.userAgent; + // We don't trust the option as it may lie in case of connectOverCDP where remote browser + // may have been launched with different options. + browser.options.headful = !version.userAgent.includes('Headless'); if (!options.persistent) { await session.send('Target.setAutoAttach', { autoAttach: true, waitForDebuggerOnStart: true, flatten: true }); return browser; @@ -123,6 +126,14 @@ export class CRBrowser extends Browser { return this._userAgent; } + _platform(): 'mac' | 'linux' | 'win' { + if (this._userAgent.includes('Windows')) + return 'win'; + if (this._userAgent.includes('Macintosh')) + return 'mac'; + return 'linux'; + } + isClank(): boolean { return this.options.name === 'clank'; } diff --git a/packages/playwright-core/src/server/chromium/crPage.ts b/packages/playwright-core/src/server/chromium/crPage.ts index ec257d1429..50e52d9a56 100644 --- a/packages/playwright-core/src/server/chromium/crPage.ts +++ b/packages/playwright-core/src/server/chromium/crPage.ts @@ -15,32 +15,32 @@ * limitations under the License. */ +import path from 'path'; +import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper'; +import { registry } from '../../utils/registry'; +import { rewriteErrorMessage } from '../../utils/stackTrace'; +import { assert, createGuid, headersArrayToObject } from '../../utils/utils'; +import * as dialog from '../dialog'; import * as dom from '../dom'; import * as frames from '../frames'; import { helper } from '../helper'; -import { eventsHelper, RegisteredListener } from '../../utils/eventsHelper'; import * as network from '../network'; -import { CRSession, CRConnection, CRSessionEvents } from './crConnection'; -import { CRExecutionContext } from './crExecutionContext'; -import { CRNetworkManager } from './crNetworkManager'; -import { Page, Worker, PageBinding } from '../page'; -import { Protocol } from './protocol'; -import { toConsoleMessageLocation, exceptionToError, releaseObject } from './crProtocolHelper'; -import * as dialog from '../dialog'; -import { PageDelegate } from '../page'; -import path from 'path'; -import { RawMouseImpl, RawKeyboardImpl, RawTouchscreenImpl } from './crInput'; -import { getAccessibilityTree } from './crAccessibility'; -import { CRCoverage } from './crCoverage'; -import { CRPDF } from './crPdf'; -import { CRBrowserContext } from './crBrowser'; -import * as types from '../types'; -import { rewriteErrorMessage } from '../../utils/stackTrace'; -import { assert, headersArrayToObject, createGuid } from '../../utils/utils'; -import { VideoRecorder } from './videoRecorder'; +import { Page, PageBinding, PageDelegate, Worker } from '../page'; import { Progress } from '../progress'; +import * as types from '../types'; +import { getAccessibilityTree } from './crAccessibility'; +import { CRBrowserContext } from './crBrowser'; +import { CRConnection, CRSession, CRSessionEvents } from './crConnection'; +import { CRCoverage } from './crCoverage'; import { DragManager } from './crDragDrop'; -import { registry } from '../../utils/registry'; +import { CRExecutionContext } from './crExecutionContext'; +import { RawKeyboardImpl, RawMouseImpl, RawTouchscreenImpl } from './crInput'; +import { CRNetworkManager } from './crNetworkManager'; +import { CRPDF } from './crPdf'; +import { exceptionToError, releaseObject, toConsoleMessageLocation } from './crProtocolHelper'; +import { platformToFontFamilies } from './defaultFontFamilies'; +import { Protocol } from './protocol'; +import { VideoRecorder } from './videoRecorder'; const UTILITY_WORLD_NAME = '__playwright_utility_world__'; @@ -79,7 +79,7 @@ export class CRPage implements PageDelegate { this._opener = opener; this._isBackgroundPage = isBackgroundPage; const dragManager = new DragManager(this); - this.rawKeyboard = new RawKeyboardImpl(client, browserContext._browser._isMac, dragManager); + this.rawKeyboard = new RawKeyboardImpl(client, browserContext._browser._platform() === 'mac', dragManager); this.rawMouse = new RawMouseImpl(this, client, dragManager); this.rawTouchscreen = new RawTouchscreenImpl(client); this._pdf = new CRPDF(client); @@ -512,6 +512,8 @@ class FrameSession { promises.push(emulateLocale(this._client, options.locale)); if (options.timezoneId) promises.push(emulateTimezone(this._client, options.timezoneId)); + if (!this._crPage._browserContext._browser.options.headful) + promises.push(this._setDefaultFontFamilies(this._client)); promises.push(this._updateGeolocation(true)); promises.push(this._updateExtraHTTPHeaders(true)); promises.push(this._updateRequestInterception()); @@ -1017,6 +1019,11 @@ class FrameSession { await this._client.send('Emulation.setEmulatedMedia', { media: this._page._state.mediaType || '', features }); } + private async _setDefaultFontFamilies(session: CRSession) { + const fontFamilies = platformToFontFamilies[this._crPage._browserContext._browser._platform()]; + await session.send('Page.setFontFamilies', fontFamilies); + } + async _updateRequestInterception(): Promise { await this._networkManager.setRequestInterception(this._page._needsRequestInterception()); } diff --git a/packages/playwright-core/src/server/chromium/defaultFontFamilies.ts b/packages/playwright-core/src/server/chromium/defaultFontFamilies.ts new file mode 100644 index 0000000000..bc1c0dbfc0 --- /dev/null +++ b/packages/playwright-core/src/server/chromium/defaultFontFamilies.ts @@ -0,0 +1,157 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Protocol } from './protocol'; + +// DO NOT EDIT: this map is generated from Chromium source code by utils/generate_chromium_default_font_families.js +export const platformToFontFamilies: { [key in 'linux'|'mac'|'win']: Protocol.Page.setFontFamiliesParameters } = { + 'linux': { + 'fontFamilies': { + 'standard': 'Times New Roman', + 'fixed': 'Monospace', + 'serif': 'Times New Roman', + 'sansSerif': 'Arial', + 'cursive': 'Comic Sans MS', + 'fantasy': 'Impact', + 'pictograph': 'Times New Roman' + } + }, + 'mac': { + 'fontFamilies': { + 'standard': 'Times', + 'fixed': 'Courier', + 'serif': 'Times', + 'sansSerif': 'Helvetica', + 'cursive': 'Apple Chancery', + 'fantasy': 'Papyrus', + 'pictograph': 'Apple Color Emoji' + }, + 'forScripts': [ + { + 'script': 'jpan', + 'fontFamilies': { + 'standard': 'Hiragino Kaku Gothic ProN', + 'fixed': 'Osaka-Mono', + 'serif': 'Hiragino Mincho ProN', + 'sansSerif': 'Hiragino Kaku Gothic ProN' + } + }, + { + 'script': 'hang', + 'fontFamilies': { + 'standard': 'Apple SD Gothic Neo', + 'serif': 'AppleMyungjo', + 'sansSerif': 'Apple SD Gothic Neo' + } + }, + { + 'script': 'hans', + 'fontFamilies': { + 'standard': ',PingFang SC,STHeiti', + 'serif': 'Songti SC', + 'sansSerif': ',PingFang SC,STHeiti', + 'cursive': 'Kaiti SC' + } + }, + { + 'script': 'hant', + 'fontFamilies': { + 'standard': ',PingFang TC,Heiti TC', + 'serif': 'Songti TC', + 'sansSerif': ',PingFang TC,Heiti TC', + 'cursive': 'Kaiti TC' + } + } + ] + }, + 'win': { + 'fontFamilies': { + 'standard': 'Times New Roman', + 'fixed': 'Consolas', + 'serif': 'Times New Roman', + 'sansSerif': 'Arial', + 'cursive': 'Comic Sans MS', + 'fantasy': 'Impact', + 'pictograph': 'Segoe UI Symbol' + }, + 'forScripts': [ + { + 'script': 'cyrl', + 'fontFamilies': { + 'standard': 'Times New Roman', + 'fixed': 'Courier New', + 'serif': 'Times New Roman', + 'sansSerif': 'Arial' + } + }, + { + 'script': 'arab', + 'fontFamilies': { + 'fixed': 'Courier New', + 'sansSerif': 'Segoe UI' + } + }, + { + 'script': 'grek', + 'fontFamilies': { + 'standard': 'Times New Roman', + 'fixed': 'Courier New', + 'serif': 'Times New Roman', + 'sansSerif': 'Arial' + } + }, + { + 'script': 'jpan', + 'fontFamilies': { + 'standard': ',Meiryo,Yu Gothic', + 'fixed': 'MS Gothic', + 'serif': ',Yu Mincho,MS PMincho', + 'sansSerif': ',Meiryo,Yu Gothic' + } + }, + { + 'script': 'hang', + 'fontFamilies': { + 'standard': 'Malgun Gothic', + 'fixed': 'Gulimche', + 'serif': 'Batang', + 'sansSerif': 'Malgun Gothic', + 'cursive': 'Gungsuh' + } + }, + { + 'script': 'hans', + 'fontFamilies': { + 'standard': 'Microsoft YaHei', + 'fixed': 'NSimsun', + 'serif': 'Simsun', + 'sansSerif': 'Microsoft YaHei', + 'cursive': 'KaiTi' + } + }, + { + 'script': 'hant', + 'fontFamilies': { + 'standard': 'Microsoft JhengHei', + 'fixed': 'MingLiU', + 'serif': 'PMingLiU', + 'sansSerif': 'Microsoft JhengHei', + 'cursive': 'DFKai-SB' + } + } + ] + } +}; diff --git a/tests/headful.spec.ts b/tests/headful.spec.ts index 37a7a6c163..563c533160 100644 --- a/tests/headful.spec.ts +++ b/tests/headful.spec.ts @@ -14,7 +14,9 @@ * limitations under the License. */ -import { playwrightTest as it, expect } from './config/browserTest'; +import pixelmatch from 'pixelmatch'; +import { PNG } from 'pngjs'; +import { expect, playwrightTest as it } from './config/browserTest'; it('should have default url when launching browser #smoke', async ({ browserType, createUserDataDir }) => { const browserContext = await browserType.launchPersistentContext(await createUserDataDir(), { headless: false }); @@ -255,3 +257,32 @@ it.skip('should click bottom row w/ infobar in OOPIF', async ({ browserType, cre await page.frames()[1].click('text=Submit'); } }); + +it('headless and headful should use same default fonts', async ({ page, headless, browserName, browserType, platform }) => { + it.info().annotations.push({ type: 'issue', description: 'https://github.com/microsoft/playwright/issues/11177' }); + it.fixme(browserName === 'firefox', 'Text is misaligned in headed vs headless'); + const genericFontFamilies = [ + 'standard', + 'serif', + 'sans-serif', + 'monospace', + 'cursive', + 'fantasy', + 'emoji' + ]; + const headedBrowser = await browserType.launch({ headless: !headless }); + const headedPage = await headedBrowser.newPage(); + for (const family of genericFontFamilies) { + const content = `
+ Lorem ipsum dolor sit amet consectetur adipiscing elit proin, integer curabitur imperdiet rhoncus cursus tincidunt bibendum, consequat sed magnis laoreet luctus mollis tellus. Nisl parturient mus accumsan feugiat sem laoreet magnis nisi, aptent per sollicitudin gravida orci ac blandit, viverra eros praesent auctor vivamus semper bibendum. Consequat sed habitasse luctus dictumst gravida platea semper phasellus, nascetur ridiculus purus est varius quisque et scelerisque, id vehicula eleifend montes sollicitudin dis velit. Pellentesque ridiculus per natoque et eleifend taciti nunc, laoreet auctor at condimentum imperdiet ante, conubia mi cubilia scelerisque sociosqu sem.

Curabitur magna per felis primis mauris non dapibus luctus ultricies eros, quis et egestas condimentum lobortis eget semper montes litora purus, ridiculus elementum sollicitudin imperdiet dictum lacinia parturient cras eu. Risus cum varius rhoncus eros torquent pretium taciti id erat dis egestas, nibh tristique montes convallis metus lacus phasellus blandit ut auctor bibendum semper, facilisis mi integer eget ultrices lobortis odio viverra duis dui. Risus ullamcorper lacinia in venenatis sodales fusce tortor potenti volutpat quis, dictum vulputate suspendisse velit mollis torquent sociis aptent morbi, senectus nascetur justo maecenas conubia magnis viverra gravida fames. Phasellus sed nec gravida nibh class augue lectus, blandit quis turpis orci diam nam pellentesque, ultricies metus imperdiet hendrerit lacinia lacus.

Inceptos facilisi montes cum hendrerit, pulvinar ut tellus eget velit, arcu nulla aenean. Phasellus augue urna nostra molestie interdum vehicula, posuere fames cum euismod massa curabitur donec, inceptos cubilia tellus facilisis fermentum. Lacus laoreet facilisis ultrices cursus quisque at ad porta vestibulum massa inceptos, curae class aliquet maecenas cum ullamcorper pulvinar erat mus vitae. Cum in aenean convallis dis quam tincidunt justo sed quisque, imperdiet faucibus hendrerit felis commodo scelerisque magnis vehicula etiam leo, eros varius platea lobortis maecenas condimentum nisi phasellus. Turpis vulputate mus himenaeos sociosqu facilisis dignissim leo quam, ultricies habitasse commodo molestie est tortor vitae et, porttitor risus erat cursus phasellus facilisi litora.

Nostra habitasse egestas magnis velit pellentesque parturient cum lectus viverra, vestibulum sociosqu nunc vel urna consequat lacinia phasellus at sapien, aenean pretium dictum sed montes interdum imperdiet iaculis. Leo hac eros arcu senectus maecenas, tortor pulvinar venenatis lacinia volutpat, mattis platea ut facilisi. Aenean condimentum at et donec sociosqu fermentum luctus potenti semper vulputate, sapien justo non est auctor gravida ultricies fames per commodo, sed habitasse facilisi nulla quisque hendrerit aliquet viverra bibendum.

Interdum nisl quam etiam montes porttitor laoreet nullam senectus velit, mauris proin tellus imperdiet litora venenatis fames massa quis, sollicitudin justo vivamus curae in sociis suscipit facilisi. Platea inceptos lacus elementum pellentesque quam euismod dictumst sociis tincidunt vulputate porttitor eros, turpis netus ut ad tempor sapien aliquet sodales molestie consequat nostra. Cum augue in quisque primis ut nunc sodales, sem orci tempus posuere cubilia suspendisse lacinia ligula, magna sed ridiculus at maecenas habitant.

Natoque magna ac feugiat tellus bibendum diam, metus lobortis nisl ornare varius praesent, dictumst gravida lacus parturient semper. Pellentesque faucibus congue fusce posuere placerat dictum vitae, dui vestibulum eu sociis tempus aliquam ultricies malesuada, potenti laoreet lacus sem gravida nisi. Nostra platea sagittis hendrerit congue conubia senectus bibendum quis sapien pharetra, scelerisque nam imperdiet fermentum feugiat suspendisse viverra luctus at, semper ac consequat vitae mi gravida parturient mollis nascetur. Vel taciti justo consequat primis et blandit convallis sed, felis purus fusce a venenatis etiam aenean scelerisque, fringilla volutpat sagittis egestas rutrum id dis.

Feugiat fermentum tortor ante ac iaculis sollicitudin ut interdum, cras orci ullamcorper potenti tristique vehicula. Molestie tortor ullamcorper rutrum turpis malesuada phasellus sem ultricies praesent mattis lobortis porta, senectus venenatis diam nostra laoreet volutpat per aptent justo elementum cum. Urna cursus vel felis cras eleifend arcu enim magnis, duis rutrum nibh nascetur cubilia interdum ultrices curae, id lacus aliquam dictumst diam fringilla lacinia.

Luctus diam morbi eget tellus libero taciti faucibus inceptos, natoque facilisis lectus maecenas risus dapibus suscipit nibh, vel curae conubia orci imperdiet metus fusce. Condimentum massa donec luctus pharetra cum, in viverra placerat nisl litora facilisis, neque nascetur sociis dictumst. Suscipit accumsan eget rhoncus pharetra justo malesuada aliquet, suspendisse metus eleifend tincidunt varius ridiculus, convallis primis vitae curabitur quis mus.

Gravida donec lacus molestie tortor aenean ultricies blandit per tempor, nostra penatibus orci vestibulum semper lectus vel a, montes potenti cum dapibus natoque eu volutpat nulla. Himenaeos purus nam malesuada habitasse nisl pharetra laoreet feugiat mi non, ultrices ultricies a cras ante eu venenatis ligula. Suscipit ut mus habitasse at aliquet sodales commodo justo, feugiat platea sagittis phasellus eleifend pellentesque interdum iaculis, integer cubilia montes metus hendrerit tincidunt purus.

Vel posuere tellus dapibus eget duis cubilia, nec class vehicula libero gravida ligula, tempus urna taciti donec congue. Facilisis ridiculus congue cum dui per augue natoque, molestie hac etiam pellentesque dignissim urna class, feugiat aenean massa himenaeos penatibus ut eu, convallis purus et fusce tempus mattis. At mattis suscipit porta nostra nec facilisis sodales turpis, integer et lectus conubia justo nam congue taciti odio, fermentum semper cubilia fusce nunc purus velit. +

+ `; + await Promise.all([page.setContent(content), headedPage.setContent(content)]); + const [image1, image2] = (await Promise.all([ + page.screenshot(), headedPage.screenshot() + ])).map(buffer => PNG.sync.read(buffer)); + const count = pixelmatch(image1.data, image2.data, null, image1.width, image2.height, { threshold: 0.01 }); + expect(count).toBe(0); + } + await headedBrowser.close(); +}); diff --git a/utils/generate_chromium_default_font_families.js b/utils/generate_chromium_default_font_families.js new file mode 100644 index 0000000000..6c401678c0 --- /dev/null +++ b/utils/generate_chromium_default_font_families.js @@ -0,0 +1,117 @@ +/** + * Copyright (c) Microsoft Corporation. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check +const xml2js = require('xml2js'); +const fs = require('fs'); +const path = require('path'); +const { argv } = require('process'); + +// From https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/prefs/prefs_tab_helper.cc;l=130;drc=62b77bef90de54f0136b51935fa2d5814a1b4da9 +// and https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/platform/text/locale_to_script_mapping.cc;l=44;drc=befcb6de95fb8c88c162ce1f64111f6c17351b13 +// note that some suffixes like _JAPANESE, _KOREAN don't have matching icu codes. +const codeToScriptName = new Map([ + ['ARABIC', 'arab'], + ['CYRILLIC', 'cyrl'], + ['GREEK', 'grek'], + ['JAPANESE', 'jpan'], + ['KOREAN', 'hang'], + ['SIMPLIFIED_HAN', 'hans'], + ['TRADITIONAL_HAN', 'hant'], +]); + +const idToProtocol = new Map([ + ['IDS_STANDARD_FONT_FAMILY', 'standard'], + ['IDS_SANS_SERIF_FONT_FAMILY','sansSerif'], + ['IDS_SERIF_FONT_FAMILY', 'serif'], + ['IDS_CURSIVE_FONT_FAMILY', 'cursive'], + ['IDS_FANTASY_FONT_FAMILY', 'fantasy'], + ['IDS_FIXED_FONT_FAMILY', 'fixed'], + ['IDS_PICTOGRAPH_FONT_FAMILY', 'pictograph'] +]); + +class ScriptFontFamilies { + scriptToFontFamilies = new Map(); + + setFont(scriptName, familyName, value) { + let fontFamilies = this.scriptToFontFamilies.get(scriptName); + if (!fontFamilies) { + fontFamilies = {}; + this.scriptToFontFamilies.set(scriptName, fontFamilies); + } + fontFamilies[familyName] = value; + } + + toJSON() { + const forScripts = Array.from(this.scriptToFontFamilies.entries()).filter(([name, _]) => !!name).map(([script, fontFamilies]) => ({ script, fontFamilies })); + return { + fontFamilies: this.scriptToFontFamilies.get(''), + forScripts: forScripts.length ? forScripts : undefined + }; + } +} + +if (argv.length < 3) + throw new Error('Expected path to "chromium/src" checkout as first argument') + +// Upstream files location is https://chromium.googlesource.com/chromium/src/+/main/chrome/app/resources/locale_settings_linux.grd +const resourceDir = path.join(argv[2], 'chrome/app/resources/'); +if (!fs.existsSync(resourceDir)) + throw new Error(`Path ${resourceDir} does not exist`); + +function parseXML(xml) { + let result; + xml2js.parseString(xml, {trim: true}, (err, r) => result = r); + return result; +} + +const result = {}; +for (const platform of ['linux', 'mac', 'win']) { + const f = path.join(resourceDir, `locale_settings_${platform}.grd`); + const xmlDataStr = fs.readFileSync(f); + let jsonObj = parseXML(xmlDataStr); + if (!jsonObj) + throw new Error('Failed to parse ' + f); + const fontFamilies = new ScriptFontFamilies(); + const defaults = jsonObj.grit.release[0].messages[0].message; + defaults.forEach(e => { + const name = e['$']['name']; + let scriptName = ''; + let familyName; + for (const id of idToProtocol.keys()) { + if (!name.startsWith(id)) + continue; + familyName = idToProtocol.get(id); + if (name !== id) { + const suffix = name.substring(id.length + 1); + // We don't support this, see https://source.chromium.org/chromium/chromium/src/+/main:chrome/browser/ui/prefs/prefs_tab_helper.cc;l=384-390;drc=62b77bef90de54f0136b51935fa2d5814a1b4da9 + if (suffix === 'ALT_WIN') + continue; + scriptName = codeToScriptName.get(suffix); + if (!scriptName) + throw new Error('NO Script name for: ' + suffix); + } + break; + } + // Skip things like IDS_NTP_FONT_FAMILY, IDS_MINIMUM_FONT_SIZE etc. + if (!familyName) + return; + fontFamilies.setFont(scriptName, familyName, e['_']) + }); + result[platform] = fontFamilies.toJSON(); +} + +console.log(JSON.stringify(result, null, 2).replaceAll('"', `'`));