this repo has no description
1
fork

Configure Feed

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

at main 388 lines 12 kB view raw
1import { file, glob } from "astro/loaders"; 2import type { ZodNullable, ZodObject, ZodRawShape, ZodString } from "astro/zod"; 3import { defineCollection, reference, z } from "astro:content"; 4import * as YAML from "yaml"; 5import PO from "pofile"; 6import { wakatimeCollection } from "./wakatime"; 7import { readFile } from "node:fs/promises"; 8import slug from "slug"; 9 10const translatedString = z.object({ 11 en: z.string(), 12 fr: z.string(), 13}); 14 15const nullableDate = z 16 .string() 17 .nullable() 18 .optional() 19 .transform((value) => (value ? new Date(value) : null)); 20 21const year = z.number().min(2003).max(new Date().getFullYear()).int(); 22 23export const collections = { 24 frenchMessages: gettextPoMessages("i18n/fr.po"), 25 englishMessages: gettextPoMessages("i18n/en.po"), 26 wakatimeLanguages: await wakatimeCollection( 27 ".wakatime-cache.json", 28 "languages", 29 ), 30 wakatimeProjects: await wakatimeCollection( 31 ".wakatime-cache.json", 32 "projects", 33 ), 34 blogEntries: defineCollection({ 35 loader: glob({ pattern: "*.md", base: "./blog" }), 36 schema: z.object({ 37 title: z.string(), 38 date: z.coerce.date(), 39 works: z.array(reference("works")).optional().default([]), 40 draft: z.boolean().optional().default(false), 41 }), 42 }), 43 works: defineCollection({ 44 loader: file("works.json"), 45 schema: z.object({ 46 id: z.string(), 47 builtAt: z.coerce.date(), 48 descriptionHash: z.string(), 49 source: z.string(), 50 metadata: z 51 .object({ 52 aliases: z 53 .array(z.string()) 54 .optional() 55 .nullable() 56 .default([]) 57 .transform((aliases) => { 58 if (process.platform !== "win32") return aliases; 59 // On Windows, we want to some characters that are not allowed in filenames 60 return aliases?.filter((a) => !a.match(/[<>:"/\\|?*]/)); 61 }), 62 finished: nullableDate, 63 started: nullableDate, 64 madeWith: z 65 .array(reference("technologies")) 66 .optional() 67 .nullable() 68 .default([]), 69 tags: z.array(reference("tags")).optional().nullable().default([]), 70 thumbnail: z.string().optional(), 71 thumbnailSource: z.string().optional(), 72 titleStyle: z.string().optional(), 73 colors: z.object({ 74 primary: z.string(), 75 secondary: z.string(), 76 tertiary: z.string(), 77 }), 78 pageBackground: z.string().optional(), 79 wip: z.boolean(), 80 private: z.boolean(), 81 additionalMetadata: z 82 .object({ 83 layout: z 84 .array( 85 z.union([ 86 z.string().nullable(), 87 z.array(z.string().nullable()), 88 ]), 89 ) 90 .optional(), 91 created: nullableDate, 92 title_style: z.string().optional(), 93 made_with: z.array(z.string()).optional(), 94 wakatime: z 95 .union([ 96 z.string(), 97 z.record(z.string(), z.string().nullable()), 98 z.array( 99 z.union([ 100 z.string(), 101 z.record(z.string(), z.string().nullable()), 102 ]), 103 ), 104 ]) 105 .optional() 106 .transform((names) => 107 names 108 ? typeof names === "string" 109 ? { [names]: null } 110 : Array.isArray(names) 111 ? Object.fromEntries( 112 names.map((n) => { 113 if (typeof n === "string") { 114 return [n, null]; 115 } 116 117 const [[key, value]] = Object.entries(n); 118 return [key, value]; 119 }), 120 ) 121 : names 122 : undefined, 123 ), 124 tagline: z 125 .string() 126 .optional() 127 .transform((s) => s?.trim() || undefined), 128 }) 129 .nullable() 130 .default(() => ({})), 131 databaseMetadata: z.object({ 132 Partial: z.boolean(), 133 }), 134 }) 135 .transform(({ started, finished, additionalMetadata, ...rest }) => ({ 136 started: started ?? additionalMetadata?.created ?? null, 137 finished: finished ?? additionalMetadata?.created ?? null, 138 additionalMetadata, 139 ...rest, 140 })), 141 Partial: z.boolean(), 142 content: z.record( 143 z.object({ 144 layout: z.array(z.array(z.string())), 145 title: z.string(), 146 footnotes: z.record(z.string()), 147 abbreviations: z.record(z.string()), 148 blocks: z.array( 149 z.object({ 150 id: z.string(), 151 type: z.enum(["paragraph", "media", "link"]), 152 anchor: z.string(), 153 index: z.number(), 154 alt: z.string(), 155 caption: z.string(), 156 relativeSource: z.string(), 157 distSource: z.string(), 158 contentType: z.string(), 159 size: z.number(), 160 dimensions: z.object({ 161 width: z.number(), 162 height: z.number(), 163 aspectRatio: z.number(), 164 }), 165 online: z.boolean(), 166 duration: z.number(), 167 hasSound: z.boolean(), 168 colors: z.object({ 169 primary: z.string(), 170 secondary: z.string(), 171 tertiary: z.string(), 172 }), 173 thumbnails: z.record(z.string()).nullable().default({}), 174 thumbnailsBuiltAt: z.coerce.date().optional(), 175 attributes: z.object({ 176 loop: z.boolean(), 177 autoplay: z.boolean(), 178 muted: z.boolean(), 179 playsinline: z.boolean(), 180 controls: z.boolean(), 181 }), 182 analyzed: z.boolean(), 183 hash: z.string(), 184 content: z.string(), 185 text: z.string(), 186 title: z.string(), 187 url: z.string(), 188 }), 189 ), 190 }), 191 ), 192 }), 193 }), 194 experiences: yamlDataCollection( 195 "experiences.yaml", 196 z.object({ 197 time: z.union([year, z.tuple([year, year])]), 198 title: z.string(), 199 links: z.array(z.string().url()).optional().default([]), 200 company: z.object({ 201 name: z.string(), 202 url: z.string().url().optional(), 203 description: z.string(), 204 }), 205 location: z.string(), 206 description: z.string(), 207 skills: z.array(z.string()).optional(), 208 }), 209 (exp) => `${exp.company.name}, ${exp.time}`, 210 ), 211 education: yamlDataCollection( 212 "education.yaml", 213 z.object({ 214 time: year, 215 title: z.string(), 216 school: z.object({ 217 name: z.string(), 218 url: z.string().url().optional(), 219 }), 220 location: z.string(), 221 diploma: z.object({ 222 results: z 223 .object({ 224 location: z.string().optional(), 225 scores: z.record(z.tuple([z.string(), z.string()])), 226 }) 227 .optional(), 228 name: z.string(), 229 }), 230 }), 231 (exp) => exp.time.toString(), 232 ), 233 sites: yamlDataCollection( 234 "sites.yaml", 235 z.object({ 236 name: z.string(), 237 url: z.string().url(), 238 purpose: z.string().optional(), 239 username: z.string().optional(), 240 slug: z.string(), 241 }), 242 (site) => site.name, 243 ), 244 collections: yamlDataCollection( 245 "collections.yaml", 246 z.object({ 247 title: translatedString, 248 description: translatedString.optional(), 249 includes: z.string(), 250 singular: z.string().optional(), 251 plural: z.string().optional(), 252 }), 253 ), 254 tags: yamlDataCollection( 255 "tags.yaml", 256 z.object({ 257 isAliasOf: z.string().nullable().default(null), 258 slug: z.string(), 259 singular: z.string(), 260 plural: z.string(), 261 description: z.string().optional(), 262 "learn more at": z.string().url().optional(), 263 detect: z 264 .object({ 265 "made with": z.array(reference("technologies")).optional(), 266 }) 267 .optional(), 268 }), 269 (tag) => tag.plural, 270 (tag) => [...tag.aliases, tag.singular], 271 ), 272 technologies: yamlDataCollection( 273 "technologies.yaml", 274 z.object({ 275 isAliasOf: z.string().nullable().default(null), 276 aliases: z.array(z.string()).optional().default([]), 277 slug: z.string(), 278 name: z.string(), 279 by: z.string().optional(), 280 files: z.array(z.string()).optional(), 281 "learn more at": z.string().url().optional(), 282 description: z.string().optional(), 283 autodetect: z.array(z.string()).optional(), 284 includes: z.array(z.string()).optional(), 285 }), 286 (tech) => tech.name, 287 (tech) => tech.aliases, 288 ), 289}; 290 291function yamlDataCollection< 292 Shape extends ZodRawShape, 293 Schema extends ZodObject<Shape>, 294>( 295 filename: string, 296 schema: Schema, 297 slugify?: (data: z.infer<Schema>) => string, 298 aliases?: (data: z.infer<Schema> & { aliases: string[] }) => string[], 299) { 300 return defineCollection({ 301 schema, 302 loader: { 303 name: "YAML Loader", 304 async load({ renderMarkdown, store, generateDigest, watcher }) { 305 const raw = await readFile(filename); 306 const parsed: Array<z.infer<Schema>> | Record<string, z.infer<Schema>> = 307 YAML.parse(raw.toString()); 308 309 watcher?.add(filename); 310 311 if (Array.isArray(parsed)) { 312 const entries = parsed.flatMap((data) => { 313 const out: z.infer<Schema> & { 314 slug?: string; 315 } = { ...data }; 316 317 const aliasIds = aliases?.({ aliases: [], ...data }) ?? []; 318 319 if (slugify) out.slug ??= slugify(data); 320 321 return [ 322 { ...out, isAliasOf: null }, 323 ...aliasIds 324 .filter((slug) => slug !== out.slug) 325 .map((slug) => ({ 326 ...out, 327 slug, 328 isAliasOf: out.slug, 329 })), 330 ]; 331 }); 332 333 store.clear(); 334 for (const entry of entries) { 335 store.set({ 336 id: entry.slug ?? slug(entry.title), 337 data: entry, 338 digest: generateDigest(JSON.stringify(entry)), 339 rendered: await renderMarkdown( 340 "description" in entry 341 ? typeof entry.description === "string" 342 ? entry.description 343 : entry.description[process.env.LANG === "fr" ? "fr" : "en"] 344 : "", 345 ), 346 }); 347 } 348 } else { 349 store.clear(); 350 for (const [key, data] of Object.entries(parsed)) { 351 store.set({ 352 id: key, 353 data: data as z.infer<Schema>, 354 digest: generateDigest(JSON.stringify(data)), 355 rendered: await renderMarkdown( 356 "description" in data 357 ? typeof data.description === "string" 358 ? data.description 359 : data.description[process.env.LANG === "fr" ? "fr" : "en"] 360 : "", 361 ), 362 }); 363 } 364 } 365 }, 366 }, 367 }); 368} 369 370function gettextPoMessages(filename: string) { 371 return defineCollection({ 372 schema: z.object({ 373 msgid: z.string(), 374 msgstr: z.string(), 375 msgctxt: z.string().optional(), 376 }), 377 loader: file(filename, { 378 parser(text) { 379 return PO.parse(text).items.map((item) => ({ 380 id: item.msgid, 381 ...item, 382 msgstr: item.msgstr[0], 383 msgctxt: item.msgctxt || undefined, 384 })); 385 }, 386 }), 387 }); 388}