[READ-ONLY] Mirror of https://github.com/mrgnw/cv.
1import type { CVData, Experience, Project } from "../types";
2import JSON5 from 'json5';
3
4/**
5 * Dynamically import all JSON files in the versions directory.
6 */
7const versionFiles = {
8 ...import.meta.glob<string>('/src/lib/versions/*.json', { query: '?raw', import: 'default', eager: true }),
9 ...import.meta.glob<string>('/src/lib/versions/*.json5', { query: '?raw', import: 'default', eager: true }),
10 ...import.meta.glob<string>('/src/lib/versions/*.jsonc', { query: '?raw', import: 'default', eager: true }),
11};
12
13/**
14 * Maps each version slug to its corresponding CV data.
15 */
16const versionMap: Record<string, CVData> = {};
17
18for (const path in versionFiles) {
19 const slugMatch = path.match(/\/src\/lib\/versions\/(.+?)\.(json[c5]?)$/);
20 if (slugMatch) {
21 const [, slug] = slugMatch;
22 try {
23 const content = versionFiles[path];
24 if (!content) {
25 console.error(`No content found for ${path}`);
26 continue;
27 }
28 // Remove BOM if present
29 const cleanContent = content.replace(/^\uFEFF/, '');
30 const isSpanish = slug.endsWith('.es');
31 const baseSlug = isSpanish ? slug.slice(0, -3) : slug;
32 versionMap[slug] = {
33 ...JSON5.parse(cleanContent),
34 pdfLink: isSpanish ?
35 (baseSlug === 'main' ? '/es/morgan-williams.pdf' : `/es/morgan-williams.${baseSlug}.pdf`) :
36 (baseSlug === 'main' ? '/morgan-williams.pdf' : `/morgan-williams.${baseSlug}.pdf`),
37 };
38 } catch (error) {
39 console.error(`Error parsing ${path}:`, error);
40 throw error;
41 }
42 }
43}
44
45/**
46 * Retrieves a specific version by its slug.
47 * @param slug - The slug identifier for the version.
48 * @returns The corresponding CVData or null if not found.
49 */
50export function getVersion(slug: string): CVData | null {
51 return versionMap[slug] || null;
52}
53
54/**
55 * Combines the main version with a specified version.
56 * @param slug - The slug identifier for the version to combine.
57 * @returns The merged CVData or null if merging isn't possible.
58 */
59export function coalesceVersion(slug: string): CVData | null {
60 const main = versionMap["main"];
61 const version = getVersion(slug);
62
63 if (!main || !version) {
64 return null;
65 }
66
67 // Import and parse projects
68 const projectFiles = {
69 ...import.meta.glob<string>('/src/lib/projects.jsonc', { query: '?raw', import: 'default', eager: true }),
70 ...import.meta.glob<string>('/src/lib/projects-es.jsonc', { query: '?raw', import: 'default', eager: true }),
71 };
72
73 const getProjectsPath = (slug: string) => `/src/lib/projects${slug.endsWith('.es') ? '-es' : ''}.jsonc`;
74
75 const projectContent = projectFiles[getProjectsPath(slug)];
76 if (!projectContent) {
77 console.error(`Could not find projects file for ${slug}`);
78 throw new Error('Projects file not found');
79 }
80
81 // Remove BOM if present and parse
82 const cleanProjectContent = projectContent.replace(/^\uFEFF/, '');
83 const projectsList = JSON5.parse(cleanProjectContent) as Project[];
84
85 // Create projects map
86 const projectsMap = new Map<string, Project>(
87 projectsList.map(project => [project.name, project])
88 );
89
90 function resolveProjects(projects: (string | Project)[] | undefined): Project[] {
91 if (!projects) return [];
92
93 return projects.map(project => {
94 if (typeof project === "string") {
95 // If project is specified by name, look it up in projectsMap
96 const fullProject = projectsMap.get(project);
97 if (!fullProject) {
98 console.warn(`Project "${project}" not found in projects.jsonc`);
99 return null;
100 }
101 return fullProject;
102 }
103 // If it's a full project definition, use it as is
104 return project;
105 }).filter((p): p is Project => p !== null);
106 }
107
108 // Create merged object without projects first
109 const { projects: mainProjects = [], ...mainRest } = main;
110 const { projects: versionProjects = [], ...versionRest } = version;
111 const merged: CVData = { ...mainRest, ...versionRest };
112
113 // Resolve projects from version only
114 merged.projects = resolveProjects(versionProjects || []);
115
116 // Merge experience sections if they exist in both
117 if (main.experience && version.experience) {
118 merged.experience = mergeExperiences(main.experience, version.experience);
119 }
120
121 return merged;
122}
123
124/**
125 * Merges two arrays of Experience objects by their index.
126 * @param mainExperiences - The primary array of experiences.
127 * @param versionExperiences - The array of experiences to merge from the version.
128 * @returns A new array of merged Experience objects.
129 */
130function mergeExperiences(
131 mainExperiences: Experience[],
132 versionExperiences: Experience[]
133): Experience[] {
134 const maxLength = Math.max(mainExperiences.length, versionExperiences.length);
135 const mergedExperiences: Experience[] = [];
136
137 for (let i = 0; i < maxLength; i++) {
138 const mainExp = mainExperiences[i];
139 const versionExp = versionExperiences[i];
140
141 if (mainExp && versionExp) {
142 mergedExperiences.push({
143 ...mainExp,
144 ...versionExp,
145 description: mergeDescriptions(mainExp.description, versionExp.description),
146 });
147 } else if (versionExp) {
148 mergedExperiences.push(versionExp);
149 } else if (mainExp) {
150 mergedExperiences.push(mainExp);
151 }
152 }
153
154 return mergedExperiences;
155}
156
157/**
158 * Merges two arrays of description strings line by line.
159 * Prefer version descriptions over main descriptions when available.
160 * @param mainDescription - The primary array of descriptions.
161 * @param versionDescription - The array of descriptions to merge from the version.
162 * @returns A new array of merged description strings.
163 */
164function mergeDescriptions(
165 mainDescription: string[],
166 versionDescription: string[]
167): string[] {
168 const maxLength = Math.max(mainDescription.length, versionDescription.length);
169 const mergedDescription: string[] = [];
170
171 for (let i = 0; i < maxLength; i++) {
172 const versionLine = typeof versionDescription[i] === "string"
173 ? versionDescription[i].trim()
174 : "";
175 mergedDescription[i] = versionLine || mainDescription[i] || "";
176 }
177
178 return mergedDescription;
179}
180
181/**
182 * Retrieves all available versions.
183 * @returns An array of slugs representing all versions.
184 */
185export function getAllVersions(): string[] {
186 return Object.keys(versionMap);
187}