Spaces:
Running
Running
| 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, '"').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); } | |
| }); | |
| } | |