import os from 'os'; import path from 'path'; import fs from 'fs'; import crypto from 'crypto'; export function tempPath(prefix, ext) { const id = Date.now().toString(36) + '-' + crypto.randomBytes(4).toString('hex'); return path.join(os.tmpdir(), `${prefix}-${id}.${ext}`); } export function escapeXml(s) { return ('' + s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"').replace(/'/g, '''); } export function escapeText(s) { return ('' + s).replace(/:/g, '\\:').replace(/'/g, "\\'").replace(/\\/g, '\\\\'); } export function resolveAudioPath(audioKey) { if (!audioKey) return null; const key = audioKey; const assetsDir = path.join(process.cwd(), 'public', 'assets', 'audio-effects'); if (fs.existsSync(assetsDir)) { const files = fs.readdirSync(assetsDir); const match = files.find(f => path.parse(f).name === key); if (match) return path.join(assetsDir, match); } // fallback: if key is already a path if (path.isAbsolute(key) && fs.existsSync(key)) return key; const candidate = path.join(process.cwd(), key); if (fs.existsSync(candidate)) return candidate; return null; } // Ensure font file for given fontName exists in public/assets/fonts (or public root). If missing, try downloading from Google Fonts. export async function ensureFontFile(fontName, fontWeight) { if (!fontName) return null; const assetsFontsDir = path.join(process.cwd(), 'public', 'assets', 'fonts'); if (!fs.existsSync(assetsFontsDir)) fs.mkdirSync(assetsFontsDir, { recursive: true }); const normalized = s => ('' + s).replace(/\s+/g, '').toLowerCase(); const target = ('' + fontName).toLowerCase(); // check assets/fonts const files = fs.readdirSync(assetsFontsDir); let match = files.find(f => { const name = path.parse(f).name; return name.toLowerCase() === target || normalized(name) === normalized(fontName) || name.toLowerCase().includes(target); }); if (!match && fontWeight) { const weightNormalized = ('' + fontWeight).toLowerCase(); match = files.find(f => { const name = path.parse(f).name.toLowerCase(); return name.includes(weightNormalized) || name.includes(target.replace(/\s+/g, '').toLowerCase() + weightNormalized) || name.includes(target + '-' + weightNormalized); }); } if (match) return path.join(assetsFontsDir, match); // check public root const publicRootFiles = fs.readdirSync(process.cwd()).filter(f => /\.(ttf|otf|woff2?|woff)$/i.test(f)); match = publicRootFiles.find(f => { const name = path.parse(f).name; return name.toLowerCase() === target || normalized(name) === normalized(fontName) || name.toLowerCase().includes(target); }); if (!match && fontWeight) { const weightNormalized = ('' + fontWeight).toLowerCase(); match = publicRootFiles.find(f => { const name = path.parse(f).name.toLowerCase(); return name.includes(weightNormalized) || name.includes(target.replace(/\s+/g, '').toLowerCase() + weightNormalized) || name.includes(target + '-' + weightNormalized); }); } if (match) return path.join(process.cwd(), match); // Try downloading from Google Fonts - best-effort with redirect support try { // Build Google Fonts family parameter. Include weight when provided so CSS contains specific weight ranges. const familyBase = fontName.trim().replace(/\s+/g, '+'); const familyParam = fontWeight ? `${familyBase}:wght@${fontWeight}` : familyBase; const cssUrl = `https://fonts.googleapis.com/css2?family=${familyParam}&display=swap`; console.log('Attempting to download font from Google Fonts:', cssUrl); const css = await fetchUrl(cssUrl, 5); console.log('Fetched CSS for font:', cssUrl); if (css) { // find first url(...) occurrence and extract URL const urlMatches = css.match(/url\((['"]?)[^'"\)]+\1\)/g) || []; let fontUrl = null; for (const m of urlMatches) { let u = m.replace(/^url\((['"]?)/, '').replace(/(['"]?)\)$/, ''); if (u.startsWith('//')) u = 'https:' + u; if (/^https?:\/\//i.test(u)) { fontUrl = u; break; } } if (fontUrl) { const parsed = new URL(fontUrl); const ext = path.parse(parsed.pathname).ext || '.woff2'; const weightSuffix = fontWeight ? `_${String(fontWeight).replace(/\s+/g, '')}` : ''; const outFile = path.join(assetsFontsDir, `${fontName.replace(/\s+/g, '_')}${weightSuffix}${ext}`); await downloadUrl(fontUrl, outFile, 5); return outFile; } } } catch (e) { // ignore download errors } return null; } function fetchUrl(url, redirects = 5) { return new Promise((resolve, reject) => { try { const https = require('https'); const http = require('http'); const doGet = (u, remaining) => { const client = u.startsWith('https:') ? https : http; const opts = { headers: { 'User-Agent': 'curl/7.64.1' } }; client.get(u, opts, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && remaining > 0) { const loc = res.headers.location.startsWith('http') ? res.headers.location : (u.startsWith('https:') ? 'https:' + res.headers.location : res.headers.location); return doGet(loc, remaining - 1); } if (res.statusCode && res.statusCode >= 400) return reject(new Error('Failed to fetch ' + u + ' status ' + res.statusCode)); let data = ''; res.setEncoding('utf8'); res.on('data', chunk => data += chunk); res.on('end', () => resolve(data)); }).on('error', reject); }; doGet(url, redirects); } catch (e) { reject(e); } }); } function downloadUrl(url, outPath, redirects = 5) { return new Promise((resolve, reject) => { try { const https = require('https'); const http = require('http'); const doGet = (u, remaining) => { const client = u.startsWith('https:') ? https : http; client.get(u, (res) => { if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location && remaining > 0) { const loc = res.headers.location.startsWith('http') ? res.headers.location : (u.startsWith('https:') ? 'https:' + res.headers.location : res.headers.location); return doGet(loc, remaining - 1); } if (res.statusCode && res.statusCode >= 400) return reject(new Error('Failed to download ' + u + ' status ' + res.statusCode)); const file = fs.createWriteStream(outPath); res.pipe(file); file.on('finish', () => file.close(() => resolve(outPath))); file.on('error', (err) => { try { fs.unlinkSync(outPath); } catch (e) { }; reject(err); }); }).on('error', (err) => reject(err)); }; doGet(url, redirects); } catch (e) { reject(e); } }); }