@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.
- package/README.md +52 -131
- package/dist/check.mjs +20 -11
- package/dist/chunks/authoring.mjs +372 -0
- package/dist/{shared/vitakit.CypCF-4Q.mjs → chunks/bibliography.mjs} +74 -237
- package/dist/chunks/build.mjs +178 -0
- package/dist/chunks/documents.mjs +152 -0
- package/dist/chunks/draft.mjs +222 -0
- package/dist/chunks/eject.mjs +59 -0
- package/dist/chunks/emit.mjs +216 -0
- package/dist/chunks/eta.mjs +16 -0
- package/dist/chunks/fields.mjs +79 -0
- package/dist/chunks/inventory.mjs +201 -0
- package/dist/chunks/migrations.mjs +218 -0
- package/dist/chunks/plan.mjs +172 -0
- package/dist/chunks/prompt.mjs +33 -0
- package/dist/chunks/publication-lists.mjs +202 -0
- package/dist/chunks/schema-source.mjs +12 -0
- package/dist/chunks/schemas.mjs +109 -0
- package/dist/chunks/sections.mjs +109 -0
- package/dist/chunks/snippets.mjs +199 -0
- package/dist/chunks/validate.mjs +133 -0
- package/dist/chunks/vue.mjs +153 -0
- package/dist/chunks/yaml.mjs +62 -0
- package/dist/cli.d.mts +33 -40
- package/dist/cli.mjs +7 -224
- package/dist/index.d.mts +379 -49
- package/dist/index.mjs +15 -7
- package/dist/nuxt.mjs +48 -10
- package/dist/runtime/components/VitaPublications.vue +21 -22
- package/dist/runtime/components/VitaSection.vue +94 -38
- package/dist/runtime/composables/useVitakit.ts +14 -3
- package/dist/schemas/education.ts +4 -4
- package/dist/schemas/employment.ts +3 -3
- package/dist/schemas/publications.ts +10 -8
- package/dist/schemas/supervision.ts +3 -3
- package/dist/schemas/talks.ts +1 -1
- package/dist/schemas/teaching.ts +3 -3
- package/dist/shared/index.d.mts +60 -9
- package/dist/shared/index.mjs +3 -57
- package/dist/shared/{vitakit.D8WRg_Xx.d.mts → vitakit.BfR_mUHm.d.mts} +25 -12
- package/dist/shared/vitakit.CCTDg7pz.mjs +199 -0
- package/dist/shared/vitakit.CfB3hMHl.mjs +60 -0
- package/dist/shared/{vitakit.BlU-p01e.mjs → vitakit.CtChKO0I.mjs} +1 -1
- package/dist/shared/vitakit.D-DdJaDZ.mjs +20 -0
- package/dist/shared/vitakit.D10HyDhF.mjs +19 -0
- package/dist/shared/vitakit.DCK90zq4.mjs +67 -0
- package/dist/shared/vitakit.DQ6PmGoL.mjs +34 -0
- package/dist/shared/vitakit.DcQOdM4I.mjs +57 -0
- package/dist/shared/vitakit.DteA9vLy.mjs +135 -0
- package/dist/shared/vitakit.DvABWuUc.mjs +121 -0
- package/dist/shared/vitakit.DvvUYUTS.mjs +200 -0
- package/dist/shared/vitakit.TSMTC5Jl.mjs +6 -0
- package/dist/shared/vitakit.UfoXdL7U.mjs +22 -0
- package/dist/shared/vitakit.zwr1ssO5.mjs +1850 -0
- package/dist/tex/templates/_generic.eta +6 -4
- package/dist/tex/templates/education.eta +10 -5
- package/dist/tex/templates/employment.eta +12 -5
- package/dist/tex/templates/publications.eta +7 -3
- package/dist/tex/templates/supervision.eta +10 -2
- package/dist/tex/templates/talks.eta +5 -1
- package/dist/tex/templates/teaching.eta +6 -4
- package/dist/tex/vitakit-cv.cls +36 -10
- package/dist/tex.d.mts +80 -11
- package/dist/tex.mjs +27 -117
- package/package.json +10 -7
- package/src/bin/vitakit.mjs +3 -153
- package/dist/shared/vitakit.BRhjqpAt.mjs +0 -250
- package/dist/shared/vitakit.BxcnlFGP.mjs +0 -325
- package/src/bin/vitakit-build-cv.mjs +0 -50
- package/src/bin/vitakit-check.mjs +0 -57
|
@@ -1,22 +1,22 @@
|
|
|
1
|
-
import {
|
|
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
|
|
4
|
-
import {
|
|
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 {
|
|
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
|
-
|
|
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
|
|
109
|
-
|
|
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 = {
|
|
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
|
|
119
|
-
const ids =
|
|
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, `
|
|
122
|
-
const
|
|
123
|
-
if (!ids.includes(
|
|
124
|
-
fail(where, `
|
|
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
|
-
|
|
128
|
-
|
|
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,
|
|
138
|
-
*
|
|
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
|
-
|
|
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
|
|
148
|
-
|
|
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
|
-
|
|
240
|
-
|
|
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
|
|
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
|
|
262
|
-
const known = new Set(
|
|
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(", ")} (
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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) => ({ "&": "&", "<": "<", ">": ">" })[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 {
|
|
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 };
|