@anja.codes/vitakit 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +52 -131
  2. package/dist/check.mjs +20 -11
  3. package/dist/chunks/authoring.mjs +372 -0
  4. package/dist/{shared/vitakit.CypCF-4Q.mjs → chunks/bibliography.mjs} +74 -237
  5. package/dist/chunks/build.mjs +178 -0
  6. package/dist/chunks/documents.mjs +152 -0
  7. package/dist/chunks/draft.mjs +222 -0
  8. package/dist/chunks/eject.mjs +59 -0
  9. package/dist/chunks/emit.mjs +216 -0
  10. package/dist/chunks/eta.mjs +16 -0
  11. package/dist/chunks/fields.mjs +79 -0
  12. package/dist/chunks/inventory.mjs +201 -0
  13. package/dist/chunks/migrations.mjs +218 -0
  14. package/dist/chunks/plan.mjs +172 -0
  15. package/dist/chunks/prompt.mjs +33 -0
  16. package/dist/chunks/publication-lists.mjs +202 -0
  17. package/dist/chunks/schema-source.mjs +12 -0
  18. package/dist/chunks/schemas.mjs +109 -0
  19. package/dist/chunks/sections.mjs +109 -0
  20. package/dist/chunks/snippets.mjs +199 -0
  21. package/dist/chunks/validate.mjs +133 -0
  22. package/dist/chunks/vue.mjs +153 -0
  23. package/dist/chunks/yaml.mjs +62 -0
  24. package/dist/cli.d.mts +33 -40
  25. package/dist/cli.mjs +7 -224
  26. package/dist/index.d.mts +379 -49
  27. package/dist/index.mjs +15 -7
  28. package/dist/nuxt.mjs +48 -10
  29. package/dist/runtime/components/VitaPublications.vue +21 -22
  30. package/dist/runtime/components/VitaSection.vue +94 -38
  31. package/dist/runtime/composables/useVitakit.ts +14 -3
  32. package/dist/schemas/education.ts +4 -4
  33. package/dist/schemas/employment.ts +3 -3
  34. package/dist/schemas/publications.ts +10 -8
  35. package/dist/schemas/supervision.ts +3 -3
  36. package/dist/schemas/talks.ts +1 -1
  37. package/dist/schemas/teaching.ts +3 -3
  38. package/dist/shared/index.d.mts +60 -9
  39. package/dist/shared/index.mjs +3 -57
  40. package/dist/shared/{vitakit.D8WRg_Xx.d.mts → vitakit.BfR_mUHm.d.mts} +25 -12
  41. package/dist/shared/vitakit.CCTDg7pz.mjs +199 -0
  42. package/dist/shared/vitakit.CfB3hMHl.mjs +60 -0
  43. package/dist/shared/{vitakit.BlU-p01e.mjs → vitakit.CtChKO0I.mjs} +1 -1
  44. package/dist/shared/vitakit.D-DdJaDZ.mjs +20 -0
  45. package/dist/shared/vitakit.D10HyDhF.mjs +19 -0
  46. package/dist/shared/vitakit.DCK90zq4.mjs +67 -0
  47. package/dist/shared/vitakit.DQ6PmGoL.mjs +34 -0
  48. package/dist/shared/vitakit.DcQOdM4I.mjs +57 -0
  49. package/dist/shared/vitakit.DteA9vLy.mjs +135 -0
  50. package/dist/shared/vitakit.DvABWuUc.mjs +121 -0
  51. package/dist/shared/vitakit.DvvUYUTS.mjs +200 -0
  52. package/dist/shared/vitakit.TSMTC5Jl.mjs +6 -0
  53. package/dist/shared/vitakit.UfoXdL7U.mjs +22 -0
  54. package/dist/shared/vitakit.zwr1ssO5.mjs +1850 -0
  55. package/dist/tex/templates/_generic.eta +6 -4
  56. package/dist/tex/templates/education.eta +10 -5
  57. package/dist/tex/templates/employment.eta +12 -5
  58. package/dist/tex/templates/publications.eta +7 -3
  59. package/dist/tex/templates/supervision.eta +10 -2
  60. package/dist/tex/templates/talks.eta +5 -1
  61. package/dist/tex/templates/teaching.eta +6 -4
  62. package/dist/tex/vitakit-cv.cls +36 -10
  63. package/dist/tex.d.mts +80 -11
  64. package/dist/tex.mjs +27 -117
  65. package/package.json +10 -7
  66. package/src/bin/vitakit.mjs +3 -153
  67. package/dist/shared/vitakit.BRhjqpAt.mjs +0 -250
  68. package/dist/shared/vitakit.BxcnlFGP.mjs +0 -325
  69. package/src/bin/vitakit-build-cv.mjs +0 -50
  70. package/src/bin/vitakit-check.mjs +0 -57
@@ -1,22 +1,22 @@
1
- import { readFile, stat, readdir } from 'node:fs/promises';
1
+ import { existsSync } from 'node:fs';
2
+ import { stat, readFile } from 'node:fs/promises';
2
3
  import { join, isAbsolute, resolve } from 'node:path';
3
- import matter from 'gray-matter';
4
- import { parse } from 'yaml';
4
+ import { r as resolveAsset } from '../shared/vitakit.DQ6PmGoL.mjs';
5
+ import { badgePresets, Bibliography } from '@behackl/citation-js-extras';
5
6
  import { unified } from 'unified';
7
+ import rehypeParse from 'rehype-parse';
8
+ import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
9
+ import rehypeStringify from 'rehype-stringify';
6
10
  import remarkParse from 'remark-parse';
7
11
  import remarkGfm from 'remark-gfm';
8
12
  import remarkMath from 'remark-math';
9
13
  import remarkRehype from 'remark-rehype';
10
14
  import rehypeKatex from 'rehype-katex';
11
15
  import rehypeRaw from 'rehype-raw';
12
- import rehypeStringify from 'rehype-stringify';
13
16
  import { visit } from 'unist-util-visit';
14
- import { m as fail, p as formatZodIssues, o as resolveAsset, q as importProjectModule, s as schemas, k as atMost, w as renderRichFields, x as resolveSitePaths, I as Issues, y as profile, z as absoluteSitePath, A as whereOf, B as messageOf, C as page } from './vitakit.BxcnlFGP.mjs';
15
- import { existsSync } from 'node:fs';
16
- import { badgePresets, Bibliography } from '@behackl/citation-js-extras';
17
- import rehypeParse from 'rehype-parse';
18
- import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
17
+ import { f as fail, a as formatZodIssues, I as Issues, i as importProjectModule } from '../shared/vitakit.DCK90zq4.mjs';
19
18
  import { z } from 'zod';
19
+ import { LISTS_FILE, selectPublications, publicationStatus } from './publication-lists.mjs';
20
20
 
21
21
  const URL_PROPERTIES = ["href", "src", "poster", "data", "action", "formAction"];
22
22
  const isRootRelative = (value) => value.startsWith("/") && !value.startsWith("//");
@@ -81,7 +81,7 @@ ${details}`);
81
81
  }
82
82
  function inline(source) {
83
83
  const html = markdown(source);
84
- const unwrapped = /^<p>([\s\S]*)<\/p>$/.exec(html);
84
+ const unwrapped = /^<p>((?:(?!<\/p>)[\s\S])*)<\/p>$/.exec(html);
85
85
  if (!unwrapped) throw new Error(`expected a single paragraph, got: ${html}`);
86
86
  return unwrapped[1];
87
87
  }
@@ -102,30 +102,34 @@ const badge = z.strictObject({
102
102
  className: z.string().optional()
103
103
  });
104
104
  const input = z.strictObject({
105
- groups: z.array(z.strictObject({
105
+ statuses: z.array(z.strictObject({
106
106
  id: z.string().regex(ID, "expected a lowercase id such as `preprint` or `talk-slides`"),
107
107
  label: z.string().trim().min(1)
108
- })).min(1, "expected at least one group").optional(),
109
- defaultGroup: z.string().optional(),
108
+ })).min(1, "expected at least one status").optional(),
109
+ defaultStatus: z.string().optional(),
110
110
  badges: z.array(badge).optional(),
111
111
  fields: z.array(z.string().regex(/^[a-z][a-z0-9_-]*$/i, "expected a BibTeX field name")).optional(),
112
112
  cslStyle: z.string().trim().min(1).optional()
113
113
  });
114
- function resolvePublications(value, defaults = { groups: [], defaultGroup: "", badges: [], fields: [] }, where = "schemas/publications.ts") {
114
+ function resolvePublications(value, defaults = { statuses: [], defaultStatus: "", badges: [], fields: [] }, where = "schemas/publications.ts") {
115
+ const renamed = ["groups", "defaultGroup"].filter((key) => typeof value === "object" && value !== null && Object.hasOwn(value, key));
116
+ if (renamed.length) {
117
+ fail(where, `${renamed.join(" and ")} ${renamed.length > 1 ? "are" : "is"} now called ${renamed.map((key) => key === "groups" ? "statuses" : "defaultStatus").join(" and ")}; \`vitakit migrate\` renames ${renamed.length > 1 ? "them" : "it"}`);
118
+ }
115
119
  const parsed = input.safeParse(value);
116
120
  if (!parsed.success) fail(where, `invalid
117
121
  ${formatZodIssues(parsed.error)}`);
118
- const groups = parsed.data.groups ?? defaults.groups;
119
- const ids = groups.map((group) => group.id);
122
+ const statuses = parsed.data.statuses ?? defaults.statuses;
123
+ const ids = statuses.map((status) => status.id);
120
124
  const duplicate = ids.find((id, index) => ids.indexOf(id) !== index);
121
- if (duplicate) fail(where, `group "${duplicate}" is defined twice`);
122
- const defaultGroup = parsed.data.defaultGroup ?? (parsed.data.groups ? ids[0] : defaults.defaultGroup);
123
- if (!ids.includes(defaultGroup)) {
124
- fail(where, `defaultGroup "${defaultGroup}" is not one of the groups (${ids.join(", ")})`);
125
+ if (duplicate) fail(where, `status "${duplicate}" is defined twice`);
126
+ const defaultStatus = parsed.data.defaultStatus ?? (parsed.data.statuses ? ids[0] : defaults.defaultStatus);
127
+ if (!ids.includes(defaultStatus)) {
128
+ fail(where, `defaultStatus "${defaultStatus}" is not one of the statuses (${ids.join(", ")})`);
125
129
  }
126
130
  return {
127
- groups,
128
- defaultGroup,
131
+ statuses,
132
+ defaultStatus,
129
133
  badges: parsed.data.badges ?? defaults.badges,
130
134
  fields: parsed.data.fields ?? defaults.fields,
131
135
  ...parsed.data.cslStyle ? { cslStyle: parsed.data.cslStyle } : {}
@@ -134,18 +138,19 @@ ${formatZodIssues(parsed.error)}`);
134
138
 
135
139
  const packageDefaults = publications({
136
140
  /**
137
- * The values of the `status` field, in the order the groups appear. A
138
- * status that is not listed here is an error, in any capitalisation.
141
+ * The values of the `status` field, with the heading of their group, in
142
+ * the order the groups appear in a list grouped by status. A status that
143
+ * is not listed here is an error, in any capitalisation.
139
144
  */
140
- groups: [
145
+ statuses: [
141
146
  { id: "published", label: "Published" },
142
147
  { id: "accepted", label: "Accepted" },
143
148
  { id: "preprint", label: "Preprints" },
144
149
  { id: "software", label: "Software" },
145
150
  { id: "other", label: "Other" }
146
151
  ],
147
- /** The group of an entry without `status`; the first group if left out. */
148
- defaultGroup: "published",
152
+ /** The status of an entry without one; the first status if left out. */
153
+ defaultStatus: "published",
149
154
  /**
150
155
  * BibTeX fields that become links after a reference on the website. The
151
156
  * presets accept the spellings real exports use (`doi:10...`, doi.org URLs,
@@ -168,8 +173,6 @@ const packageDefaults = publications({
168
173
  });
169
174
 
170
175
  const defaultPublications = resolvePublications(packageDefaults, void 0, "src/schemas/publications.ts");
171
- defaultPublications.badges;
172
- defaultPublications.groups;
173
176
  async function loadPublicationsSchema(rootDir) {
174
177
  const where = "schemas/publications.ts";
175
178
  const path = join(rootDir, where);
@@ -236,17 +239,28 @@ const GENRES = {
236
239
  techreport: "Technical report"
237
240
  };
238
241
  const readableGenre = (html, { variable }) => variable === "genre" ? GENRES[html.toLowerCase()] ?? html : html;
239
- async function loadPublications(bibPath, renderer, schema = defaultPublications, baseURL = "/") {
240
- const { groups, fields } = schema;
242
+ const readBib = (path) => readFile(path, "utf8").catch((error) => {
243
+ if (error.code === "ENOENT") return "";
244
+ throw error;
245
+ });
246
+ async function readBibliography(bibPath, schema) {
247
+ try {
248
+ return new Bibliography({ data: await readBib(bibPath), customFields: ["status", ...schema.fields], preserveMath: true });
249
+ } catch (error) {
250
+ throw new Error(`${bibPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
251
+ }
252
+ }
253
+ async function loadPublications(bibPath, renderer, schema, lists, baseURL = "/", issues = new Issues(true)) {
254
+ const { statuses, fields } = schema;
241
255
  const prefix = baseURL.replace(/\/$/, "");
242
256
  const withFile = (error) => new Error(`${bibPath}: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
243
257
  let bib;
244
258
  try {
245
259
  bib = new Bibliography({
246
- data: await readFile(bibPath, "utf8"),
260
+ data: await readBib(bibPath),
247
261
  cslStyle: schema.cslStyle ?? projectCslStyle(bibPath) ?? defaultCslStyle,
248
262
  // Only fields listed here survive the citation-js pipeline; they then
249
- // appear as `entry.custom.<field>`. Badges read the raw fields.
263
+ // appear as `entry.custom.<field>`. Badges and lists read the raw fields.
250
264
  customFields: ["status", ...fields],
251
265
  preserveMath: true,
252
266
  // Defaults for every formatHtml call below.
@@ -258,18 +272,15 @@ async function loadPublications(bibPath, renderer, schema = defaultPublications,
258
272
  } catch (error) {
259
273
  throw withFile(error);
260
274
  }
261
- const statusOf = (entry) => (entry.custom.status?.trim() || schema.defaultGroup).toLowerCase();
262
- const known = new Set(groups.map((group) => group.id));
275
+ const statusOf = (entry) => publicationStatus(entry, schema);
276
+ const known = new Set(statuses.map((status) => status.id));
263
277
  const unknown = bib.entries.filter((entry) => !known.has(statusOf(entry)));
264
278
  if (unknown.length) {
265
279
  throw new Error(
266
- `${bibPath}: unknown status ${[...new Set(unknown.map(statusOf))].join(", ")} on ${unknown.map((entry) => entry.key).join(", ")}; expected one of ${[...known].join(", ")} (groups are defined in schemas/publications.ts)`
280
+ `${bibPath}: unknown status ${[...new Set(unknown.map(statusOf))].join(", ")} on ${unknown.map((entry) => entry.key).join(", ")}; expected one of ${[...known].join(", ")} (statuses are defined in schemas/publications.ts)`
267
281
  );
268
282
  }
269
- const result = [];
270
- for (const group of groups) {
271
- const entries = bib.sort(bib.entries.filter((entry) => statusOf(entry) === group.id), { by: "date" });
272
- if (!entries.length) continue;
283
+ const format = (entries) => {
273
284
  let items;
274
285
  try {
275
286
  items = splitList(bib.formatHtml(entries, {
@@ -282,7 +293,7 @@ async function loadPublications(bibPath, renderer, schema = defaultPublications,
282
293
  } catch (error) {
283
294
  throw withFile(error);
284
295
  }
285
- const publications = entries.map((entry) => {
296
+ return entries.map((entry) => {
286
297
  const html = items.get(entry.key);
287
298
  if (html === void 0) throw new Error(`${bibPath}: ${entry.key} is missing from the formatted list`);
288
299
  const { title, badges } = bib.links(entry);
@@ -300,204 +311,30 @@ async function loadPublications(bibPath, renderer, schema = defaultPublications,
300
311
  }))
301
312
  };
302
313
  });
303
- result.push({ id: group.id, label: group.label, count: entries.length, entries: publications });
314
+ };
315
+ const result = {};
316
+ for (const [name, list] of Object.entries(lists)) {
317
+ result[name] = await issues.attempt(LISTS_FILE, () => {
318
+ const groups = selectPublications(bib, name, list, schema).map((group, index) => ({
319
+ id: `${name}-${index + 1}`,
320
+ ...group.label !== void 0 ? { label: group.label } : {},
321
+ count: group.entries.length,
322
+ entries: format(group.entries)
323
+ }));
324
+ return { count: groups.reduce((sum, group) => sum + group.count, 0), groups };
325
+ }, { count: 0, groups: [] });
304
326
  }
305
327
  return result;
306
328
  }
307
329
 
308
- const RESERVED = /* @__PURE__ */ new Set(["site", "profile"]);
309
- async function exists(path) {
310
- return stat(path).then(() => true, () => false);
311
- }
312
- async function loadRepoSchema(path, id) {
313
- const { mtimeMs } = await stat(path);
314
- const module = await importProjectModule(path, mtimeMs, `schemas/${id}.ts`);
315
- const schema = module.default ?? module.schema;
316
- if (!schema || typeof schema.safeParse !== "function") {
317
- fail(`schemas/${id}.ts`, "must export a Zod schema as its default export");
318
- }
319
- return schema;
320
- }
321
- const STARTER = (id) => `create schemas/${id}.ts, for example:
322
-
323
- import { z, section, dateRange } from '@anja.codes/vitakit'
324
-
325
- export default section({
326
- date: dateRange,
327
- title: z.string(),
328
- })`;
329
- async function discoverSections(rootDir) {
330
- const contentDir = join(rootDir, "content");
331
- const schemaDir = join(rootDir, "schemas");
332
- const files = (await readdir(contentDir).catch(() => fail("content/", "directory is missing"))).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).sort();
333
- const sources = [];
334
- for (const file of files) {
335
- const id = file.replace(/\.ya?ml$/, "");
336
- if (RESERVED.has(id)) continue;
337
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) {
338
- fail(`content/${file}`, "file name must be lowercase kebab-case, e.g. `research-stays.yml`");
339
- }
340
- const override = join(schemaDir, `${id}.ts`);
341
- if (await exists(override)) {
342
- sources.push({ id, file: join(contentDir, file), schemaOrigin: "repo", schema: await loadRepoSchema(override, id) });
343
- continue;
344
- }
345
- const builtin = schemas[id];
346
- if (!builtin) fail(`content/${file}`, `no schema for this file; ${STARTER(id)}`);
347
- sources.push({ id, file: join(contentDir, file), schemaOrigin: "built-in", schema: builtin });
348
- }
349
- return sources;
350
- }
351
- function byDateDesc(a, b) {
352
- const left = a.date?.from?.sortKey;
353
- const right = b.date?.from?.sortKey;
354
- if (left === right) return 0;
355
- if (!left) return 1;
356
- if (!right) return -1;
357
- return right.localeCompare(left);
358
- }
359
- async function loadSection(source, options) {
360
- const where = `content/${source.id}.yml`;
361
- const raw = await readFile(source.file, "utf8");
362
- let data;
363
- try {
364
- data = parse(raw);
365
- } catch (error) {
366
- fail(where, error instanceof Error ? error.message : String(error));
367
- }
368
- if (data === null) return [];
369
- if (!Array.isArray(data)) fail(where, "expected a list of entries");
370
- const parsed = source.schema.safeParse(data);
371
- if (!parsed.success) {
372
- const origin = source.schemaOrigin === "repo" ? ` (schema: schemas/${source.id}.ts)` : "";
373
- fail(where, `invalid${origin}
374
- ${formatZodIssues(parsed.error)}`);
375
- }
376
- return parsed.data.map((item, index) => ({ ...item, id: `${source.id}-${index}` })).filter((item) => atMost(item.visibility, options.maxVisibility)).map((item, index) => renderRichFields(item, options.renderer, String(index), where)).map((item) => resolveSitePaths(item, options.resolvePath ?? ((path) => path))).sort(byDateDesc);
377
- }
378
-
379
- const FALLBACK_PROFILE = { name: "Your Name", links: [], nav: [], footerHtml: "" };
380
- async function loadYaml(file, where) {
381
- const raw = await readFile(file, "utf8").catch(() => fail(where, "missing"));
382
- try {
383
- return parse(raw);
384
- } catch (error) {
385
- fail(where, messageOf(error));
386
- }
387
- }
388
- async function loadProfile(contentDir, baseURL, renderer, target) {
389
- const data = await loadYaml(join(contentDir, "profile.yml"), "content/profile.yml");
390
- const parsed = profile.safeParse(data);
391
- if (!parsed.success) fail("content/profile.yml", `invalid
392
- ${formatZodIssues(parsed.error)}`);
393
- const prefix = baseURL.replace(/\/$/, "");
394
- const { avatar, nav, footer, ...rest } = parsed.data;
395
- return resolveSitePaths({
396
- ...rest,
397
- ...avatar ? { avatar: prefixRootRelative(avatar, prefix) } : {},
398
- nav: nav.map((item) => ({ label: item.label, url: item.to })),
399
- footerHtml: footer ? renderer.inline(footer) : ""
400
- }, sitePathResolver(target, baseURL, parsed.data.url));
401
- }
402
- function sitePathResolver(target, baseURL, siteUrl) {
403
- const prefix = baseURL.replace(/\/$/, "");
404
- return target === "pdf" ? (path) => absoluteSitePath(path, siteUrl) : (path) => prefixRootRelative(path, prefix);
405
- }
406
- function errorPage(route, where, message) {
407
- const escaped = message.replace(/[&<>]/g, (character) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[character]);
408
- return {
409
- route,
410
- title: `Error in ${where}`,
411
- titleHtml: `Error in <code>${where}</code>`,
412
- order: -1,
413
- bodyHtml: `<pre>${escaped}</pre>`,
414
- error: message
415
- };
416
- }
417
- async function loadPage(dir, file, renderer) {
418
- const where = `content/pages/${file}`;
419
- const slug = file.slice(0, -".md".length);
420
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
421
- fail(where, "file name must be lowercase kebab-case, e.g. `research-interests.md`");
422
- }
423
- const { data, content } = matter(await readFile(join(dir, file), "utf8"));
424
- const parsed = page.safeParse(data);
425
- if (!parsed.success) fail(where, `invalid front matter
426
- ${formatZodIssues(parsed.error)}`);
427
- const route = parsed.data.route ?? (slug === "index" ? "/" : `/${slug}/`);
428
- try {
429
- return {
430
- route,
431
- title: parsed.data.title,
432
- titleHtml: renderer.inline(parsed.data.title),
433
- description: parsed.data.description,
434
- order: parsed.data.order,
435
- bodyHtml: content.trim() ? renderer.markdown(content) : ""
436
- };
437
- } catch (error) {
438
- fail(where, messageOf(error));
439
- }
440
- }
441
- async function loadPages(contentDir, renderer, issues) {
442
- const dir = join(contentDir, "pages");
443
- const files = (await readdir(dir).catch(() => [])).filter((name) => name.endsWith(".md")).sort();
444
- const pages = await Promise.all(files.map(async (file) => {
445
- try {
446
- return await loadPage(dir, file, renderer);
447
- } catch (error) {
448
- if (issues.strict) throw error;
449
- const where = whereOf(error, `content/pages/${file}`);
450
- const message = messageOf(error);
451
- issues.record(where, message);
452
- const slug = file.slice(0, -".md".length);
453
- return errorPage(slug === "index" ? "/" : `/${slug}/`, where, message);
454
- }
455
- }));
456
- const seen = /* @__PURE__ */ new Map();
457
- for (const [index, page] of pages.entries()) {
458
- const previous = seen.get(page.route);
459
- if (previous) issues.record(`content/pages/${files[index]}`, `route ${page.route} is already used by ${previous}`);
460
- else seen.set(page.route, `content/pages/${files[index]}`);
461
- }
462
- return pages.sort((a, b) => a.order - b.order || a.route.localeCompare(b.route));
463
- }
464
- async function loadSections(rootDir, maxVisibility, renderer, resolvePath, issues) {
465
- const sources = await issues.attempt("content/", () => discoverSections(rootDir), []);
466
- const sections = {};
467
- for (const source of sources) {
468
- sections[source.id] = await issues.attempt(
469
- `content/${source.id}.yml`,
470
- () => loadSection(source, { maxVisibility, renderer, resolvePath }),
471
- []
472
- );
473
- }
474
- return sections;
475
- }
476
- async function loadContent(rootDir, options = {}) {
477
- const strict = options.strict ?? true;
478
- const baseURL = options.baseURL ?? "/";
479
- const maxVisibility = options.maxVisibility ?? "public";
480
- const issues = new Issues(strict);
481
- const renderer = createRenderer({ baseURL });
482
- const contentDir = join(rootDir, "content");
483
- const target = options.target ?? "website";
484
- const profile = await issues.attempt("content/profile.yml", () => loadProfile(contentDir, baseURL, renderer, target), FALLBACK_PROFILE);
485
- const resolvePath = sitePathResolver(target, baseURL, profile.url);
486
- const [pages, sections, publications] = await Promise.all([
487
- loadPages(contentDir, renderer, issues),
488
- loadSections(rootDir, maxVisibility, renderer, resolvePath, issues),
489
- issues.attempt(
490
- "content/publications.bib",
491
- async () => {
492
- const bib = join(contentDir, "publications.bib");
493
- const present = await readFile(bib, "utf8").then(() => true, () => false);
494
- if (!present) return [];
495
- return loadPublications(bib, renderer, await loadPublicationsSchema(rootDir), baseURL);
496
- },
497
- []
498
- )
499
- ]);
500
- return { profile, pages, sections, publications, issues: issues.list };
501
- }
330
+ const bibliography = {
331
+ __proto__: null,
332
+ defaultCslStyle: defaultCslStyle,
333
+ defaultPublications: defaultPublications,
334
+ loadPublications: loadPublications,
335
+ loadPublicationsSchema: loadPublicationsSchema,
336
+ projectCslStyle: projectCslStyle,
337
+ readBibliography: readBibliography
338
+ };
502
339
 
503
- export { loadPublicationsSchema as a, loadContent as l, publications as p };
340
+ export { prefixRootRelative as a, loadPublications as b, createRenderer as c, defaultPublications as d, bibliography as e, loadPublicationsSchema as l, publications as p };
@@ -0,0 +1,178 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { mkdir, readFile, writeFile, rm, copyFile, rename } from 'node:fs/promises';
3
+ import { existsSync } from 'node:fs';
4
+ import process from 'node:process';
5
+ import { join, delimiter, resolve } from 'node:path';
6
+ import { l as loadContent } from '../shared/vitakit.DteA9vLy.mjs';
7
+ import { f as fail } from '../shared/vitakit.DCK90zq4.mjs';
8
+ import { discoverDocuments, validateDocumentSections, selectEntries } from './documents.mjs';
9
+ import { p as publicationPlacementProblems } from '../shared/vitakit.DvvUYUTS.mjs';
10
+ import { discoverSections } from './sections.mjs';
11
+ import { emitDocument, bundledTexDir, SITE_PATH_HELP } from './emit.mjs';
12
+ import { a as escapeTex, b as escapeUrl } from '../shared/vitakit.DcQOdM4I.mjs';
13
+ import { i as isSitePath } from '../shared/vitakit.D10HyDhF.mjs';
14
+ import 'gray-matter';
15
+ import 'yaml';
16
+ import './bibliography.mjs';
17
+ import '../shared/vitakit.DQ6PmGoL.mjs';
18
+ import 'node:url';
19
+ import '@behackl/citation-js-extras';
20
+ import 'unified';
21
+ import 'rehype-parse';
22
+ import 'rehype-sanitize';
23
+ import 'rehype-stringify';
24
+ import 'remark-parse';
25
+ import 'remark-gfm';
26
+ import 'remark-math';
27
+ import 'remark-rehype';
28
+ import 'rehype-katex';
29
+ import 'rehype-raw';
30
+ import 'unist-util-visit';
31
+ import 'zod';
32
+ import './publication-lists.mjs';
33
+ import './schemas.mjs';
34
+ import '../shared/vitakit.DvABWuUc.mjs';
35
+ import '../shared/vitakit.TSMTC5Jl.mjs';
36
+ import '../shared/vitakit.D-DdJaDZ.mjs';
37
+ import 'eta';
38
+ import '../shared/vitakit.CfB3hMHl.mjs';
39
+
40
+ const CLASS_FILE = "vitakit-cv.cls";
41
+ class MissingToolError extends Error {
42
+ constructor(tool) {
43
+ super(`${tool} not found; install a TeX distribution that includes latexmk and biber`);
44
+ this.tool = tool;
45
+ this.name = "MissingToolError";
46
+ }
47
+ }
48
+ function run(command, args, cwd, env = {}, output = "inherit", log) {
49
+ return new Promise((resolve2, reject) => {
50
+ const child = spawn(command, args, { cwd, stdio: output === "inherit" ? "inherit" : ["ignore", "pipe", "pipe"], env: { ...process.env, ...env } });
51
+ let captured = "";
52
+ child.stdout?.on("data", (chunk) => {
53
+ captured += chunk.toString();
54
+ });
55
+ child.stderr?.on("data", (chunk) => {
56
+ captured += chunk.toString();
57
+ });
58
+ child.on("error", (error) => reject(
59
+ error.code === "ENOENT" ? new MissingToolError(command) : error
60
+ ));
61
+ child.on("exit", (code) => {
62
+ if (code === 0) return resolve2();
63
+ if (output === "inherit") return reject(new Error(`${command} exited with code ${code}`));
64
+ void readFile(log ?? "", "utf8").catch(() => "").then((text) => {
65
+ const lines = text.split("\n");
66
+ const start = lines.findIndex((line) => line.startsWith("! "));
67
+ const excerpt = start === -1 ? captured.trim().split("\n").slice(-25).join("\n") : lines.slice(start, start + 8).join("\n").trim();
68
+ reject(new Error(`${command} exited with code ${code}${excerpt ? `
69
+
70
+ ${excerpt}` : ""}${log ? `
71
+
72
+ Full log: ${log}` : ""}`));
73
+ });
74
+ });
75
+ });
76
+ }
77
+ function profilePreamble(content, document, lists) {
78
+ const { profile } = content;
79
+ const href = (url) => {
80
+ if (isSitePath(url)) fail("content/profile.yml", `links: ${url} ${SITE_PATH_HELP}`);
81
+ return escapeUrl(url);
82
+ };
83
+ const affiliation = profile.affiliation?.label ? escapeTex(profile.affiliation.label) : "";
84
+ const tagline = [profile.title && escapeTex(profile.title), affiliation].filter(Boolean).join(", ");
85
+ const contact = [
86
+ profile.email && `\\href{mailto:${escapeUrl(profile.email)}}{${escapeTex(profile.email)}}`,
87
+ ...profile.links.map((link) => `\\href{${href(link.url)}}{${escapeTex(link.label)}}`)
88
+ ].filter(Boolean).join(" \\quad ");
89
+ return [
90
+ `% Generated by vitakit. Do not edit; edit content/profile.yml or cv-tex/${document.name}.yml instead.`,
91
+ `\\vitaname{${escapeTex(profile.name)}}`,
92
+ `\\vitatagline{${tagline}}`,
93
+ `\\vitacontact{${contact}}`,
94
+ ...profile.url ? siteUrlSourcemap(profile.url) : [],
95
+ ...Object.entries(content.sections).flatMap(([id, entries]) => {
96
+ const title = document.options.sections[id]?.title;
97
+ return [
98
+ ...selectEntries(entries, document.options, id).length ? [] : [`\\vitaemptysection{${id}}`],
99
+ ...title ? [`\\vitasectiontitle{${id}}{${escapeTex(title)}}`] : []
100
+ ];
101
+ }),
102
+ ...lists.filter((name) => !content.publications[name]?.count).map((name) => `\\vitaemptypublications{${name}}`),
103
+ ""
104
+ ].join("\n");
105
+ }
106
+ function siteUrlSourcemap(siteUrl) {
107
+ const base = siteUrl.replace(/\/?$/, "/");
108
+ if (!/^https?:\/\/[\w.:/-]+$/.test(base)) {
109
+ fail("content/profile.yml", `url: ${siteUrl} may only contain letters, digits and - . _ : /`);
110
+ }
111
+ return [
112
+ "\\ifdefined\\DeclareSourcemap",
113
+ ` \\DeclareSourcemap{\\maps[datatype=bibtex]{\\map{\\step[fieldsource=url, match=\\regexp{\\A/}, replace={${base}}]}}}`,
114
+ "\\fi"
115
+ ];
116
+ }
117
+ async function buildDocument(document, content, options, cvDir) {
118
+ const outDir = join(cvDir, "generated", document.name);
119
+ const lists = [...new Set(document.publications.map((placement) => placement.list))];
120
+ const written = await emitDocument({
121
+ sections: content.sections,
122
+ publications: Object.fromEntries(lists.map((name) => [name, content.publications[name]])),
123
+ siteUrl: content.profile.url,
124
+ options: document.options,
125
+ templateDir: join(cvDir, "templates"),
126
+ outDir
127
+ });
128
+ await writeFile(join(outDir, "profile.tex"), profilePreamble(content, document, lists));
129
+ const generatedClass = join(outDir, CLASS_FILE);
130
+ if (existsSync(join(cvDir, CLASS_FILE))) await rm(generatedClass, { force: true });
131
+ else await copyFile(join(bundledTexDir, CLASS_FILE), generatedClass);
132
+ options.onProgress?.({ document: document.name, step: "tex", dir: outDir, files: written.length + 1 });
133
+ if (options.noPdf) return;
134
+ const relativeOut = join("generated", document.name);
135
+ await run("latexmk", [
136
+ "-pdf",
137
+ "-halt-on-error",
138
+ "-interaction=nonstopmode",
139
+ `-outdir=${relativeOut}`,
140
+ // Otherwise -outdir can shadow teaching.tex with its generated section.
141
+ resolve(document.texFile)
142
+ ], cvDir, {
143
+ // A trailing delimiter keeps the standard search paths.
144
+ TEXINPUTS: `.${delimiter}${relativeOut}${delimiter}${process.env.TEXINPUTS ?? ""}`
145
+ }, options.latexOutput, join(outDir, `${document.name}.log`));
146
+ await rename(join(outDir, `${document.name}.pdf`), join(cvDir, `${document.name}.pdf`));
147
+ options.onProgress?.({ document: document.name, step: "pdf", file: join(cvDir, `${document.name}.pdf`) });
148
+ }
149
+ async function buildCv(options) {
150
+ const cvDir = join(options.rootDir, "cv-tex");
151
+ if (!existsSync(cvDir)) {
152
+ throw new Error(`no cv-tex/ directory in ${options.rootDir}`);
153
+ }
154
+ const content = await loadContent(options.rootDir, { strict: true, maxVisibility: "private", target: "pdf" });
155
+ const documents = await discoverDocuments(cvDir);
156
+ const sources = await discoverSections(options.rootDir);
157
+ for (const document of documents) {
158
+ validateDocumentSections(document, sources);
159
+ const [problem] = publicationPlacementProblems([], [...document.placements, ...document.publications], Object.keys(content.publications));
160
+ if (problem) fail(problem.where, problem.message);
161
+ }
162
+ const unknown = options.only?.filter((name) => !documents.some((document) => document.name === name)) ?? [];
163
+ if (unknown.length) {
164
+ const known = documents.map((document) => document.name).join(", ");
165
+ throw new Error(`no document matched ${unknown.join(", ")}; available: ${known}`);
166
+ }
167
+ const selected = options.only?.length ? documents.filter((document) => options.only.includes(document.name)) : documents;
168
+ await mkdir(join(cvDir, "generated"), { recursive: true });
169
+ for (const document of selected) {
170
+ await buildDocument(document, content, options, cvDir);
171
+ }
172
+ return selected.map((document) => join(cvDir, `${document.name}.pdf`));
173
+ }
174
+ async function readGenerated(rootDir, document, section) {
175
+ return readFile(join(rootDir, "cv-tex", "generated", document, `${section}.tex`), "utf8");
176
+ }
177
+
178
+ export { MissingToolError, buildCv, readGenerated };