Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 133 additions & 45 deletions scripts/populate-icon-defs.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,42 @@
'use strict'

// populate-icon-defs.js
// =============
//
// Scans the Antora output in public/**/*.html for all uses of Fontawesome icons like:
//
// <i class="fas fa-copy"></i>
//
// 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 {
Expand All @@ -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 = '<i class="fa'
const ICON_RX = /<i class="fa[brs]? fa-[^" ]+/g
const REQUIRED_ICON_NAMES_RX = /\biconNames: *(\[.*?\])/

// on all *.html files under dir, run the provided function fn, and collate all results in a Set.
// e.g. all values will be unique

function runOnHtmlFiles (dir, fn) {
return fsp.readdir(dir, { withFileTypes: true }).then((dirents) => {
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
//
// <i class="fas fa-copy"></i>
// <i class="far fa-save"></i>
//
// 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))
Copy link

Copilot AI Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use substring() instead of the deprecated substr() method. Replace with it.substring(10).

Suggested change
.map((it) => it.substr(10))
.map((it) => it.substring(10))

Copilot uses AI. Check for mistakes.
}
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"));
Copy link

Copilot AI Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use substring() instead of the deprecated substr() method. Replace with contents.substring(0, contents.indexOf('\n')).

Suggested change
let firstLine = contents.substr(0, contents.indexOf("\n"));
let firstLine = contents.substring(0, contents.indexOf("\n"));

Copilot uses AI. Check for mistakes.

try {
const requiredIconNames = JSON.parse(firstLine.match(REQUIRED_ICON_NAMES_RX)[1].replace(/'/g, '"'))
console.log(requiredIconNames)
Copy link

Copilot AI Sep 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this debug console.log statement as it appears to be leftover from debugging and should not be in production code.

Suggested change
console.log(requiredIconNames)

Copilot uses AI. Check for mistakes.
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')
})()