diff --git a/scripts/populate-icon-defs.js b/scripts/populate-icon-defs.js index 8cb5f35ef2..d94c9060ca 100644 --- a/scripts/populate-icon-defs.js +++ b/scripts/populate-icon-defs.js @@ -1,15 +1,42 @@ 'use strict' +// populate-icon-defs.js +// ============= +// +// Scans the Antora output in public/**/*.html for all uses of Fontawesome icons like: +// +// +// +// Collates these together, and writes a file to +// +// public/_/js/vendor/populate-icon-defs.js +// +// that contains the full SVG icon definitions for each of these icons. +// This is used by the UI to substitue the icon images at runtime. +// +// NOTE: the docs-ui/ bundle contains a default version of this file, which is only periodically +// updated. +// This script will however work on the *actual* output, so will honour newly added icons. +// // Prerequisite: +// ============= // -// $ npm --no-package-lock i +// $ npm ci // // Usage: +// ============= // -// $ node populate-icon-defs.js ../public +// $ node scripts/populate-icon-defs.js public // -const { promises: fsp } = require('fs') + +// NOTE: original version of script was async, refactored to synchronous to simplify debugging +// Against a problematically large input that crashed our staging build, the dumb sync version +// takes around 7 seconds. +// We could reintroduce async code here to optimize, in due course. + +const fs = require('fs') const ospath = require('path') + const iconPacks = { fas: (() => { try { @@ -34,69 +61,130 @@ const iconPacks = { })(), fab: require('@fortawesome/free-brands-svg-icons'), } + iconPacks.fa = iconPacks.fas const iconShims = require('@fortawesome/fontawesome-free/js/v4-shims').reduce((accum, it) => { accum['fa-' + it[0]] = [it[1] || 'fas', 'fa-' + (it[2] || it[0])] return accum }, {}) +// define patterns/regular expressions used in the scanning const ICON_SIGNATURE_CS = ' { - return dirents.reduce(async (accum, dirent) => { - const entries = dirent.isDirectory() - ? await runOnHtmlFiles(ospath.join(dir, dirent.name), fn) - : (dirent.name.endsWith('.html') ? await fn(ospath.join(dir, dirent.name)) : undefined) - return entries && entries.length ? (await accum).concat(entries) : accum - }, []) - }) + const ret = new Set() + const files = findHtmlFiles(dir) + for (const path of files) { + const val = fn(path) + if (val) { + for (const item of val) { + ret.add(item) + } + } + } + return ret +} + +// return a list of all HTML files (recursive, e.g. **/*.html) +function findHtmlFiles (dir) { + const ret = [] + + for (const dirent of fs.readdirSync(dir, { withFileTypes: true })) { + if (dirent.isDirectory()) { + const files = findHtmlFiles(ospath.join(dir, dirent.name)) + ret.push(...files) + + } else if (dirent.name.endsWith('.html')) { + ret.push(ospath.join(dir, dirent.name)) + } + } + return ret } function camelCase (str) { return str.replace(/-(.)/g, (_, l) => l.toUpperCase()) } +// Return all icon names +// e.g. for example, an HTML file that contained these icon definitions +// +// +// +// +// Would return ["fas fa-copy", "far fa-save"] + +function getScannedNames(path) { + const contents = fs.readFileSync(path) + if (contents.includes(ICON_SIGNATURE_CS)) { + return contents.toString() + .match(ICON_RX) + .map((it) => it.substr(10)) + } + else { + return undefined + } +} + function scanForIconNames (dir) { - return runOnHtmlFiles(dir, (path) => - fsp.readFile(path).then((contents) => - contents.includes(ICON_SIGNATURE_CS) - ? contents.toString().match(ICON_RX).map((it) => it.substr(10)) - : undefined - ) - ).then((scanResult) => [...new Set(scanResult)]) + const scanResult = runOnHtmlFiles(dir, getScannedNames) + return [...scanResult] // Set to array } -;(async () => { +// On running the script, execute the following immediately invoked function expression (IIFE) +;(() => { + const siteDir = process.argv[2] || 'public' + + let iconNames = scanForIconNames(siteDir) + const iconDefsFile = ospath.join(siteDir, '_/js/vendor/fontawesome-icon-defs.js') - const iconDefs = await scanForIconNames(siteDir).then((iconNames) => - fsp.readFile(iconDefsFile, 'utf8').then((contents) => { - try { - const requiredIconNames = JSON.parse(contents.match(REQUIRED_ICON_NAMES_RX)[1].replace(/'/g, '"')) - iconNames = [...new Set(iconNames.concat(requiredIconNames))] - } catch (e) {} - }).then(() => - iconNames.reduce((accum, iconKey) => { - const [iconPrefix, iconName] = iconKey.split(' ').slice(0, 2) - let iconDef = (iconPacks[iconPrefix] || {})[camelCase(iconName)] - if (iconDef) { - return accum.set(iconKey, { ...iconDef, prefix: iconPrefix }) - } else if (iconPrefix === 'fa') { - const [realIconPrefix, realIconName] = iconShims[iconName] || [] - if ( - realIconName && - !accum.has((iconKey = `${realIconPrefix} ${realIconName}`)) && - (iconDef = (iconPacks[realIconPrefix] || {})[camelCase(realIconName)]) - ) { - return accum.set(iconKey, { ...iconDef, prefix: realIconPrefix }) - } + // first we read the stub file. This starts with a comment with a list of icons that must *always* be included + // e.g. + // /*! iconNames: ['far fa-copy', 'fas fa-link', 'fab fa-github', 'fas fa-terminal', 'fal fa-external-link-alt'] */ + + let contents = fs.readFileSync(iconDefsFile, 'utf8') + let firstLine = contents.substr(0, contents.indexOf("\n")); + + try { + const requiredIconNames = JSON.parse(firstLine.match(REQUIRED_ICON_NAMES_RX)[1].replace(/'/g, '"')) + console.log(requiredIconNames) + iconNames = [...new Set(iconNames.concat(requiredIconNames))] + } catch (e) { + // we didn't get a valid list of requiredIconNames, so don't write it back out to the new file + firstLine = undefined + } + + const iconDefs = new Map() + + for (const iconKey of iconNames) { + const [iconPrefix, iconName] = iconKey.split(' ').slice(0, 2) + let iconDef = (iconPacks[iconPrefix] || {})[camelCase(iconName)] + + if (iconDef) { + iconDefs.set(iconKey, { ...iconDef, prefix: iconPrefix }) + } + else if (iconPrefix === 'fa') { + const [realIconPrefix, realIconName] = iconShims[iconName] || [] + if (realIconName) { + const realIconKey = `${realIconPrefix} ${realIconName}` + if ( + !iconDefs.has(realIconKey) && + (iconDef = (iconPacks[realIconPrefix] || {})[camelCase(realIconName)])) + { + iconDefs.set(realIconKey, { ...iconDef, prefix: realIconPrefix }) } - return accum - }, new Map()) - ) - ) - await fsp.writeFile(iconDefsFile, `window.FontAwesomeIconDefs = ${JSON.stringify([...iconDefs.values()])}\n`, 'utf8') + } + } + } + + // update the contents to the collated example, and write it out + contents = + `${firstLine ? firstLine + "\n" : ''}window.FontAwesomeIconDefs = ${JSON.stringify([...iconDefs.values()])}\n` + + fs.writeFileSync(iconDefsFile, contents, 'utf8') })()