[READ-ONLY] Mirror of https://github.com/mrgnw/cv.
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