Publish Docker Image / build-and-push (./build/Dockerfile, git.lzstealth.com/lzstealth/pulse-signage-web, web) (push) Successful in 1m15s
Publish Docker Image / build-and-push (./build/Dockerfile.player, git.lzstealth.com/lzstealth/pulse-signage-player, player) (push) Successful in 32s
408 lines
14 KiB
JavaScript
408 lines
14 KiB
JavaScript
const fs = require('fs');
|
|
const path = require('path');
|
|
const crypto = require('crypto');
|
|
|
|
const FONT_LIBRARY_DIR_NAME = 'fonts';
|
|
const FONT_MANIFEST_FILE = 'fonts.json';
|
|
const FONT_STYLESHEET_FILE = 'fonts.css';
|
|
const DEFAULT_FONT_FAMILY_FORMATS = [
|
|
'Arial=Arial,Helvetica,sans-serif',
|
|
'Comic Sans MS=Comic Sans MS,cursive,sans-serif',
|
|
'Courier New=Courier New,Courier,monospace',
|
|
'Georgia=Georgia,serif',
|
|
'Helvetica=Helvetica,Arial,sans-serif',
|
|
'Impact=Impact,Charcoal,sans-serif',
|
|
'Lucida Sans Unicode=Lucida Sans Unicode,Lucida Grande,sans-serif',
|
|
'Palatino Linotype=Palatino Linotype,Book Antiqua,Palatino,serif',
|
|
'Tahoma=Tahoma,Geneva,sans-serif',
|
|
'Times New Roman=Times New Roman,Times,serif',
|
|
'Trebuchet MS=Trebuchet MS,Helvetica,sans-serif',
|
|
'Verdana=Verdana,Geneva,sans-serif'
|
|
];
|
|
const FONT_FILE_EXTENSIONS = new Map([
|
|
['.woff2', 'woff2'],
|
|
['.woff', 'woff'],
|
|
['.ttf', 'truetype'],
|
|
['.otf', 'opentype']
|
|
]);
|
|
|
|
function normalizeText(value) {
|
|
return String(value || '').replace(/\s+/g, ' ').trim();
|
|
}
|
|
|
|
function quoteFontFamilyToken(token) {
|
|
const normalizedToken = normalizeText(token);
|
|
if (!normalizedToken) {
|
|
return '';
|
|
}
|
|
|
|
if (normalizedToken === 'inherit' || /^[a-zA-Z0-9_-]+$/.test(normalizedToken)) {
|
|
return normalizedToken;
|
|
}
|
|
|
|
return `'${normalizedToken.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
|
|
}
|
|
|
|
function normalizeFontFamilyFormat(format) {
|
|
return String(format || '')
|
|
.split(',')
|
|
.map(quoteFontFamilyToken)
|
|
.filter(Boolean)
|
|
.join(',');
|
|
}
|
|
|
|
function normalizeFontFamilyFormatEntry(entry) {
|
|
const value = String(entry || '').trim();
|
|
if (!value) {
|
|
return '';
|
|
}
|
|
|
|
const separatorIndex = value.indexOf('=');
|
|
if (separatorIndex === -1) {
|
|
return `${value}=${normalizeFontFamilyFormat(value)}`;
|
|
}
|
|
|
|
const label = value.slice(0, separatorIndex).trim();
|
|
const format = value.slice(separatorIndex + 1).trim();
|
|
return `${label}=${normalizeFontFamilyFormat(format || label)}`;
|
|
}
|
|
|
|
function resolveMediaRoot(mediaRootOrUploadDir) {
|
|
const resolved = path.resolve(String(mediaRootOrUploadDir || '').trim());
|
|
return path.basename(resolved) === 'uploads' ? path.dirname(resolved) : resolved;
|
|
}
|
|
|
|
function getFontLibraryDir(mediaRootOrUploadDir) {
|
|
return path.join(resolveMediaRoot(mediaRootOrUploadDir), FONT_LIBRARY_DIR_NAME);
|
|
}
|
|
|
|
function getFontManifestPath(mediaRootOrUploadDir) {
|
|
return path.join(getFontLibraryDir(mediaRootOrUploadDir), FONT_MANIFEST_FILE);
|
|
}
|
|
|
|
function getFontStylesheetPath(mediaRootOrUploadDir) {
|
|
return path.join(getFontLibraryDir(mediaRootOrUploadDir), FONT_STYLESHEET_FILE);
|
|
}
|
|
|
|
function getFontStylesheetHref(mediaRootOrUploadDir) {
|
|
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
|
if (!fs.existsSync(stylesheetPath)) {
|
|
return '';
|
|
}
|
|
|
|
const stat = fs.statSync(stylesheetPath);
|
|
return `/media/${FONT_LIBRARY_DIR_NAME}/${FONT_STYLESHEET_FILE}?v=${Number(stat.mtimeMs) || Date.now()}`;
|
|
}
|
|
|
|
function normalizeFontEntry(entry) {
|
|
const fileName = normalizeText(entry && entry.fileName);
|
|
const family = normalizeText(entry && (entry.family || entry.name || entry.displayName));
|
|
const extension = path.extname(fileName).toLowerCase();
|
|
|
|
return {
|
|
id: normalizeText(entry && entry.id) || fileName,
|
|
name: family || fileName.replace(/\.[^.]+$/, ''),
|
|
family: family || fileName.replace(/\.[^.]+$/, ''),
|
|
fileName: fileName,
|
|
format: normalizeText(entry && entry.format) || FONT_FILE_EXTENSIONS.get(extension) || '',
|
|
enabled: entry && entry.enabled === false ? false : true,
|
|
createdAt: normalizeText(entry && entry.createdAt),
|
|
modifiedAt: normalizeText(entry && entry.modifiedAt)
|
|
};
|
|
}
|
|
|
|
function sortFonts(left, right) {
|
|
return String(left && left.name || '').localeCompare(String(right && right.name || ''), undefined, { sensitivity: 'base' })
|
|
|| String(left && left.fileName || '').localeCompare(String(right && right.fileName || ''), undefined, { sensitivity: 'base' });
|
|
}
|
|
|
|
function readManifest(mediaRootOrUploadDir) {
|
|
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
|
if (!fs.existsSync(manifestPath)) {
|
|
return [];
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(fs.readFileSync(manifestPath, 'utf8') || '[]');
|
|
const list = Array.isArray(parsed) ? parsed : Array.isArray(parsed && parsed.fonts) ? parsed.fonts : [];
|
|
return list.map(normalizeFontEntry).filter(function (entry) {
|
|
return Boolean(entry.fileName) && Boolean(normalizeText(entry.family || entry.name));
|
|
}).sort(sortFonts);
|
|
} catch (_error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function buildFontFaceRule(entry) {
|
|
const family = normalizeText(entry.family || entry.name);
|
|
const fileName = normalizeText(entry.fileName);
|
|
if (!family || !fileName) {
|
|
return '';
|
|
}
|
|
|
|
const extension = path.extname(fileName).toLowerCase();
|
|
const format = FONT_FILE_EXTENSIONS.get(extension) || normalizeText(entry.format) || 'woff2';
|
|
const escapedFamily = family.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
return [
|
|
'@font-face {',
|
|
` font-family: '${escapedFamily}';`,
|
|
` src: url('/media/${FONT_LIBRARY_DIR_NAME}/${encodeURIComponent(fileName)}') format('${format}');`,
|
|
' font-style: normal;',
|
|
' font-weight: 400;',
|
|
' font-display: swap;',
|
|
'}'
|
|
].join('\n');
|
|
}
|
|
|
|
function buildFontStylesheet(fonts) {
|
|
const rules = (Array.isArray(fonts) ? fonts : [])
|
|
.map(buildFontFaceRule)
|
|
.filter(Boolean);
|
|
return rules.length ? `/* Managed fonts */\n\n${rules.join('\n\n')}\n` : '/* Managed fonts */\n';
|
|
}
|
|
|
|
function buildFontFamilyFormats(fonts) {
|
|
const formatEntries = Array.from(new Set(DEFAULT_FONT_FAMILY_FORMATS.map(normalizeFontFamilyFormatEntry).concat((Array.isArray(fonts) ? fonts : [])
|
|
.filter(function (font) {
|
|
return font && font.enabled !== false && normalizeText(font.family || font.name);
|
|
})
|
|
.map(function (font) {
|
|
const family = normalizeText(font.family || font.name);
|
|
return `${family}=${normalizeFontFamilyFormat(family)}`;
|
|
}))))
|
|
.sort(function (left, right) {
|
|
const leftLabel = String(left || '').split('=')[0];
|
|
const rightLabel = String(right || '').split('=')[0];
|
|
return leftLabel.localeCompare(rightLabel, undefined, { sensitivity: 'base' });
|
|
});
|
|
|
|
return formatEntries.join(';');
|
|
}
|
|
|
|
async function ensureFontLibrary(mediaRootOrUploadDir) {
|
|
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
|
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
|
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
|
|
|
await fs.promises.mkdir(fontDir, { recursive: true });
|
|
|
|
if (!fs.existsSync(manifestPath)) {
|
|
await fs.promises.writeFile(manifestPath, '[]\n', 'utf8');
|
|
}
|
|
|
|
if (!fs.existsSync(stylesheetPath)) {
|
|
await fs.promises.writeFile(stylesheetPath, buildFontStylesheet([]), 'utf8');
|
|
}
|
|
|
|
return {
|
|
fonts: [],
|
|
fontDir: fontDir,
|
|
manifestPath: manifestPath,
|
|
stylesheetPath: stylesheetPath,
|
|
stylesheetHref: getFontStylesheetHref(mediaRootOrUploadDir),
|
|
fontFamilyFormats: buildFontFamilyFormats([])
|
|
};
|
|
}
|
|
|
|
function loadFontLibrary(mediaRootOrUploadDir) {
|
|
const fonts = readManifest(mediaRootOrUploadDir);
|
|
return {
|
|
fonts: fonts,
|
|
fontDir: getFontLibraryDir(mediaRootOrUploadDir),
|
|
manifestPath: getFontManifestPath(mediaRootOrUploadDir),
|
|
stylesheetPath: getFontStylesheetPath(mediaRootOrUploadDir),
|
|
stylesheetHref: getFontStylesheetHref(mediaRootOrUploadDir),
|
|
fontFamilyFormats: buildFontFamilyFormats(fonts)
|
|
};
|
|
}
|
|
|
|
async function saveFontLibrary(mediaRootOrUploadDir, fonts) {
|
|
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
|
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
|
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
|
const normalizedFonts = (Array.isArray(fonts) ? fonts : []).map(normalizeFontEntry).filter(function (entry) {
|
|
return Boolean(entry.fileName) && Boolean(normalizeText(entry.family || entry.name));
|
|
}).sort(sortFonts);
|
|
|
|
await fs.promises.mkdir(fontDir, { recursive: true });
|
|
await fs.promises.writeFile(manifestPath, JSON.stringify(normalizedFonts, null, 2) + '\n', 'utf8');
|
|
await fs.promises.writeFile(stylesheetPath, buildFontStylesheet(normalizedFonts), 'utf8');
|
|
return normalizedFonts;
|
|
}
|
|
|
|
function isSupportedFontUpload(file) {
|
|
const extension = path.extname(String(file && file.originalname || file && file.filename || '')).toLowerCase();
|
|
return FONT_FILE_EXTENSIONS.has(extension);
|
|
}
|
|
|
|
async function addFontFile(mediaRootOrUploadDir, uploadedFile, requestedFamily) {
|
|
if (!uploadedFile || !uploadedFile.path || !isSupportedFontUpload(uploadedFile)) {
|
|
throw new Error('Unsupported font upload.');
|
|
}
|
|
|
|
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
|
const fonts = readManifest(mediaRootOrUploadDir);
|
|
const family = normalizeText(requestedFamily) || normalizeText(path.basename(uploadedFile.originalname || uploadedFile.filename || '', path.extname(uploadedFile.originalname || uploadedFile.filename || '')));
|
|
if (!family) {
|
|
throw new Error('Font family is required.');
|
|
}
|
|
|
|
if (fonts.some(function (font) {
|
|
return String(font.family || font.name || '').trim().toLowerCase() === family.toLowerCase();
|
|
})) {
|
|
throw new Error('A font with that family name already exists.');
|
|
}
|
|
|
|
const extension = path.extname(uploadedFile.originalname || uploadedFile.filename || '').toLowerCase();
|
|
const slug = family.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'font';
|
|
const fileName = `${slug}-${Date.now()}-${crypto.randomUUID().slice(0, 8)}${extension}`;
|
|
const destinationPath = path.join(fontDir, fileName);
|
|
|
|
await fs.promises.mkdir(fontDir, { recursive: true });
|
|
await fs.promises.rename(uploadedFile.path, destinationPath);
|
|
|
|
const now = new Date().toISOString();
|
|
const nextFonts = fonts.concat({
|
|
id: fileName,
|
|
name: family,
|
|
family: family,
|
|
fileName: fileName,
|
|
format: FONT_FILE_EXTENSIONS.get(extension) || 'woff2',
|
|
enabled: true,
|
|
createdAt: now,
|
|
modifiedAt: now
|
|
});
|
|
|
|
await saveFontLibrary(mediaRootOrUploadDir, nextFonts);
|
|
return {
|
|
font: normalizeFontEntry(nextFonts[nextFonts.length - 1]),
|
|
fonts: loadFontLibrary(mediaRootOrUploadDir).fonts
|
|
};
|
|
}
|
|
|
|
async function deleteFontFile(mediaRootOrUploadDir, fontId) {
|
|
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
|
const fonts = readManifest(mediaRootOrUploadDir);
|
|
const normalizedId = normalizeText(fontId);
|
|
const index = fonts.findIndex(function (font) {
|
|
return String(font.id || font.fileName || '').trim() === normalizedId || String(font.fileName || '').trim() === normalizedId;
|
|
});
|
|
|
|
if (index === -1) {
|
|
return null;
|
|
}
|
|
|
|
const [removed] = fonts.splice(index, 1);
|
|
if (removed && removed.fileName) {
|
|
try {
|
|
await fs.promises.unlink(path.join(fontDir, removed.fileName));
|
|
} catch (_error) {
|
|
// ignore missing file cleanup issues
|
|
}
|
|
}
|
|
|
|
await saveFontLibrary(mediaRootOrUploadDir, fonts);
|
|
return normalizeFontEntry(removed);
|
|
}
|
|
|
|
async function setFontEnabled(mediaRootOrUploadDir, fontId, enabled) {
|
|
const fonts = readManifest(mediaRootOrUploadDir);
|
|
const normalizedId = normalizeText(fontId);
|
|
const index = fonts.findIndex(function (font) {
|
|
return String(font.id || font.fileName || '').trim() === normalizedId || String(font.fileName || '').trim() === normalizedId;
|
|
});
|
|
|
|
if (index === -1) {
|
|
return null;
|
|
}
|
|
|
|
const nextFonts = fonts.slice();
|
|
nextFonts[index] = Object.assign({}, nextFonts[index], {
|
|
enabled: Boolean(enabled),
|
|
modifiedAt: new Date().toISOString()
|
|
});
|
|
|
|
await saveFontLibrary(mediaRootOrUploadDir, nextFonts);
|
|
return normalizeFontEntry(nextFonts[index]);
|
|
}
|
|
|
|
function collectFontLibraryUploadPaths(mediaRootOrUploadDir) {
|
|
const fonts = readManifest(mediaRootOrUploadDir);
|
|
const uploadPaths = new Set();
|
|
const manifestPath = getFontManifestPath(mediaRootOrUploadDir);
|
|
const stylesheetPath = getFontStylesheetPath(mediaRootOrUploadDir);
|
|
const mediaRoot = resolveMediaRoot(mediaRootOrUploadDir);
|
|
|
|
if (manifestPath) {
|
|
uploadPaths.add('/media/' + path.relative(mediaRoot, manifestPath).replace(/\\/g, '/'));
|
|
}
|
|
if (stylesheetPath) {
|
|
uploadPaths.add('/media/' + path.relative(mediaRoot, stylesheetPath).replace(/\\/g, '/'));
|
|
}
|
|
|
|
fonts.forEach(function (font) {
|
|
if (font && font.fileName) {
|
|
uploadPaths.add(`/media/${FONT_LIBRARY_DIR_NAME}/${font.fileName}`);
|
|
}
|
|
});
|
|
|
|
return Array.from(uploadPaths);
|
|
}
|
|
|
|
async function collectFontLibraryDirectoryUploadPaths(mediaRootOrUploadDir) {
|
|
const fontDir = getFontLibraryDir(mediaRootOrUploadDir);
|
|
try {
|
|
const entries = await fs.promises.readdir(fontDir, { withFileTypes: true });
|
|
return entries
|
|
.filter(function (entry) {
|
|
return entry && entry.isFile();
|
|
})
|
|
.map(function (entry) {
|
|
return `/media/${FONT_LIBRARY_DIR_NAME}/${entry.name}`;
|
|
});
|
|
} catch (_error) {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function collectFontLibrarySyncOperations(mediaRootOrUploadDir) {
|
|
const fonts = readManifest(mediaRootOrUploadDir);
|
|
const operations = [
|
|
{ type: 'put', uploadPath: '/media/' + FONT_LIBRARY_DIR_NAME + '/' + FONT_MANIFEST_FILE },
|
|
{ type: 'put', uploadPath: '/media/' + FONT_LIBRARY_DIR_NAME + '/' + FONT_STYLESHEET_FILE }
|
|
];
|
|
|
|
fonts.forEach(function (font) {
|
|
if (!font || !font.fileName) {
|
|
return;
|
|
}
|
|
|
|
operations.push({
|
|
type: 'put',
|
|
uploadPath: `/media/${FONT_LIBRARY_DIR_NAME}/${font.fileName}`
|
|
});
|
|
});
|
|
|
|
return operations;
|
|
}
|
|
|
|
module.exports = {
|
|
ensureFontLibrary: ensureFontLibrary,
|
|
loadFontLibrary: loadFontLibrary,
|
|
saveFontLibrary: saveFontLibrary,
|
|
addFontFile: addFontFile,
|
|
deleteFontFile: deleteFontFile,
|
|
setFontEnabled: setFontEnabled,
|
|
collectFontLibraryUploadPaths: collectFontLibraryUploadPaths,
|
|
collectFontLibraryDirectoryUploadPaths: collectFontLibraryDirectoryUploadPaths,
|
|
collectFontLibrarySyncOperations: collectFontLibrarySyncOperations,
|
|
getFontStylesheetHref: getFontStylesheetHref,
|
|
buildFontStylesheet: buildFontStylesheet,
|
|
buildFontFamilyFormats: buildFontFamilyFormats,
|
|
normalizeFontFamilyFormat: normalizeFontFamilyFormat,
|
|
normalizeFontFamilyFormatEntry: normalizeFontFamilyFormatEntry,
|
|
isSupportedFontUpload: isSupportedFontUpload,
|
|
getFontLibraryDir: getFontLibraryDir,
|
|
getFontManifestPath: getFontManifestPath,
|
|
getFontStylesheetPath: getFontStylesheetPath
|
|
}; |