[READ-ONLY] Mirror of https://github.com/mrgnw/cv.
0

Configure Feed

Select the types of activity you want to include in your feed.

cv / pdf-version-reader.js
4.4 kB 167 lines
1// Node.js compatible version reader for PDF generation 2import fs from 'fs'; 3import path from 'path'; 4import JSON5 from 'json5'; 5 6/** 7 * Normalize a string to be a valid URL slug 8 */ 9function normalizeSlug(str) { 10 return str 11 .toLowerCase() 12 .trim() 13 .replace(/[\s_]+/g, '-') 14 .replace(/[^\w-]/g, '') 15 .replace(/-+/g, '-') 16 .replace(/^-+|-+$/g, ''); 17} 18 19/** 20 * Parse a version file path to extract job title and company 21 */ 22function parseVersionPath(filePath) { 23 const relativePath = path.relative('src/lib/versions', filePath); 24 const pathWithoutExt = relativePath.replace(/\.(json[c5]?)$/, ''); 25 26 // Handle main file 27 if (pathWithoutExt === 'main') { 28 return { job: null, company: null, sourceType: 'base' }; 29 } 30 31 // Check if it's nested (job/company) 32 const parts = pathWithoutExt.split(path.sep); 33 if (parts.length === 2) { 34 return { job: parts[0], company: parts[1], sourceType: 'scoped' }; 35 } else if (parts.length === 1) { 36 return { job: parts[0], company: null, sourceType: 'generic' }; 37 } 38 39 throw new Error(`Unexpected path structure: ${filePath}`); 40} 41 42/** 43 * Get all version files recursively 44 */ 45function getVersionFiles(dir) { 46 const files = []; 47 const items = fs.readdirSync(dir, { withFileTypes: true }); 48 49 for (const item of items) { 50 const fullPath = path.join(dir, item.name); 51 if (item.isDirectory()) { 52 files.push(...getVersionFiles(fullPath)); 53 } else if (/\.(json[c5]?)$/.test(item.name) && !item.name.includes('.es.')) { 54 // Skip Spanish versions as per our plan 55 files.push(fullPath); 56 } 57 } 58 59 return files; 60} 61 62/** 63 * Get all available version slugs (Node.js compatible) 64 */ 65// Internal caches to avoid re-walking filesystem repeatedly 66let _cached = null; 67let _cachedMTimeSignature = ''; 68 69function computeDirSignature(dir) { 70 const files = getVersionFiles(dir); 71 const stats = files.map(f => fs.statSync(f).mtimeMs).join('|'); 72 return stats; 73} 74 75function buildCache() { 76 const versionsDir = path.join('src', 'lib', 'versions'); 77 if (!fs.existsSync(versionsDir)) { 78 return { versions: [], meta: [], map: {} }; 79 } 80 const files = getVersionFiles(versionsDir); 81 const tempEntries = []; 82 for (const filePath of files) { 83 try { 84 const { job, company, sourceType } = parseVersionPath(filePath); 85 if (job === 'es' && !company) continue; // skip Spanish base if any 86 tempEntries.push({ filePath, job, company, sourceType }); 87 } catch (err) { 88 console.error('Error parsing version path', filePath, err); 89 } 90 } 91 const companyCount = new Map(); 92 tempEntries.forEach(e => { 93 if (e.company) { 94 const c = normalizeSlug(e.company); 95 companyCount.set(c, (companyCount.get(c) || 0) + 1); 96 } 97 }); 98 const slugConflicts = new Map(); 99 const slugToEntry = new Map(); 100 for (const entry of tempEntries) { 101 let slug; 102 if (entry.sourceType === 'base') { 103 slug = 'main'; 104 } else if (entry.company) { 105 const normalizedCompany = normalizeSlug(entry.company); 106 const normalizedJob = normalizeSlug(entry.job); 107 if (companyCount.get(normalizedCompany) === 1) { 108 slug = normalizedCompany; 109 } else { 110 slug = `${normalizedCompany}-${normalizedJob}`; 111 } 112 } else { 113 slug = normalizeSlug(entry.job); 114 } 115 if (slugConflicts.has(slug)) { 116 slugConflicts.get(slug).push(entry.filePath); 117 } else { 118 slugConflicts.set(slug, [entry.filePath]); 119 } 120 slugToEntry.set(slug, entry); 121 } 122 for (const [slug, paths] of slugConflicts) { 123 if (paths.length > 1) throw new Error(`Slug collision detected for "${slug}": ${paths.join(', ')}`); 124 } 125 const versions = Array.from(slugToEntry.keys()); 126 const meta = versions.map(slug => { 127 const entry = slugToEntry.get(slug); 128 return { 129 slug, 130 job: entry.job, 131 company: entry.company, 132 path: entry.filePath, 133 sourceType: entry.sourceType 134 }; 135 }); 136 const map = {}; 137 for (const m of meta) map[m.slug] = m.path; 138 return { versions, meta, map }; 139} 140 141function ensureCache() { 142 const versionsDir = path.join('src', 'lib', 'versions'); 143 const sig = fs.existsSync(versionsDir) ? computeDirSignature(versionsDir) : ''; 144 if (!_cached || sig !== _cachedMTimeSignature) { 145 _cached = buildCache(); 146 _cachedMTimeSignature = sig; 147 } 148} 149 150export function getAllVersions() { 151 ensureCache(); 152 return _cached.versions; 153} 154 155/** 156 * Get version metadata (simplified for PDF generation) 157 */ 158export function getAllVersionMeta() { 159 ensureCache(); 160 return _cached.meta; 161} 162 163export function getVersionFileMap() { 164 ensureCache(); 165 return { ..._cached.map }; 166} 167