@anja.codes/vitakit 0.2.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 +4 -3
- package/dist/check.mjs +14 -12
- package/dist/chunks/authoring.mjs +50 -45
- package/dist/{shared/vitakit.By5VWQ1n.mjs → chunks/bibliography.mjs} +73 -170
- package/dist/chunks/build.mjs +19 -17
- package/dist/chunks/documents.mjs +15 -6
- package/dist/chunks/draft.mjs +3 -3
- package/dist/chunks/eject.mjs +2 -2
- package/dist/chunks/emit.mjs +38 -22
- package/dist/chunks/fields.mjs +2 -2
- package/dist/chunks/inventory.mjs +29 -27
- package/dist/chunks/migrations.mjs +218 -0
- package/dist/chunks/plan.mjs +36 -8
- package/dist/chunks/publication-lists.mjs +202 -0
- package/dist/chunks/schemas.mjs +2 -2
- package/dist/chunks/sections.mjs +3 -3
- package/dist/chunks/validate.mjs +25 -16
- package/dist/chunks/vue.mjs +89 -52
- package/dist/chunks/yaml.mjs +62 -0
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +12 -10
- package/dist/nuxt.mjs +19 -11
- package/dist/runtime/components/VitaPublications.vue +21 -22
- package/dist/runtime/composables/useVitakit.ts +14 -3
- package/dist/schemas/publications.ts +9 -7
- package/dist/shared/index.d.mts +2 -2
- package/dist/shared/{vitakit.BfpxDtz1.d.mts → vitakit.BfR_mUHm.d.mts} +17 -5
- package/dist/shared/{vitakit.BZZIsWlY.mjs → vitakit.CCTDg7pz.mjs} +21 -1
- package/dist/shared/{vitakit.DbSeENVS.mjs → vitakit.DCK90zq4.mjs} +10 -1
- package/dist/shared/vitakit.DteA9vLy.mjs +135 -0
- package/dist/shared/{vitakit.BerUaxtF.mjs → vitakit.DvABWuUc.mjs} +1 -1
- package/dist/shared/{vitakit.Do5HC_MD.mjs → vitakit.DvvUYUTS.mjs} +61 -3
- package/dist/shared/vitakit.UfoXdL7U.mjs +22 -0
- package/dist/shared/{vitakit.CoWX2Z4R.mjs → vitakit.zwr1ssO5.mjs} +353 -57
- package/dist/tex/templates/publications.eta +4 -2
- package/dist/tex/vitakit-cv.cls +15 -4
- package/dist/tex.d.mts +29 -8
- package/dist/tex.mjs +13 -10
- package/package.json +1 -1
|
@@ -94,7 +94,8 @@ function commandHelp(spec, run) {
|
|
|
94
94
|
"Usage",
|
|
95
95
|
...(spec.usage ? spec.usage(run) : [usageLine(spec, run)]).map((line) => ` ${line}`),
|
|
96
96
|
"",
|
|
97
|
-
|
|
97
|
+
// Only blank lines: help that starts with an indented example keeps its indent.
|
|
98
|
+
spec.help(run).replace(/^\s*\n/, "").trimEnd(),
|
|
98
99
|
"",
|
|
99
100
|
"Options",
|
|
100
101
|
...rows.map(([flag, description]) => ` ${flag.padEnd(width)} ${description}`),
|
|
@@ -251,6 +252,8 @@ ${after.join("\n")}` : ""}
|
|
|
251
252
|
const SECTION_OPTIONS = ["page", "document", "title", "after", "like"];
|
|
252
253
|
const FIELD_OPTIONS = ["type", "several", "required", "choice"];
|
|
253
254
|
const PAGE_OPTIONS = ["title", "no-menu"];
|
|
255
|
+
const SELECTION_OPTIONS = ["where", "since", "until", "sort", "order", "group-by", "limit"];
|
|
256
|
+
const LIST_OPTIONS = ["page", "document", "title", "after", ...SELECTION_OPTIONS];
|
|
254
257
|
const COMMON = ["help", "root", "no-color", "dry-run", "yes"];
|
|
255
258
|
function onlyOptions(kind, values, allowed) {
|
|
256
259
|
const wrong = Object.keys(values).filter((name) => values[name] !== void 0 && ![...COMMON, ...allowed].includes(name));
|
|
@@ -291,7 +294,7 @@ async function addSection(context, id, values) {
|
|
|
291
294
|
const { sectionNameProblem } = await import('../chunks/sections.mjs');
|
|
292
295
|
const problem = sectionNameProblem(id);
|
|
293
296
|
if (problem) throw new CommandError(problem);
|
|
294
|
-
if (id === "publications") throw new CommandError(
|
|
297
|
+
if (id === "publications") throw new CommandError(`publications come from content/publications.bib; to show some of them, \`${run.bin("vitakit", "add publication-list <name>")}\` declares a list`);
|
|
295
298
|
const { inventory } = await import('../chunks/inventory.mjs');
|
|
296
299
|
const { Workspace } = await import('../chunks/plan.mjs');
|
|
297
300
|
const authoring = await import('../chunks/authoring.mjs');
|
|
@@ -468,6 +471,138 @@ async function addField(context, id, name, values) {
|
|
|
468
471
|
];
|
|
469
472
|
return finish(context, ws, values, { before, after });
|
|
470
473
|
}
|
|
474
|
+
function listSettings(values) {
|
|
475
|
+
const number = (name) => {
|
|
476
|
+
if (values[name] === void 0) return void 0;
|
|
477
|
+
const value = Number(values[name]);
|
|
478
|
+
if (!Number.isInteger(value)) throw new UsageError(`--${name} must be a whole number`, "add");
|
|
479
|
+
return value;
|
|
480
|
+
};
|
|
481
|
+
const where = {};
|
|
482
|
+
for (const expression of strings$1(values.where)) {
|
|
483
|
+
const equals = expression.indexOf("=");
|
|
484
|
+
const field = expression.slice(0, equals).trim();
|
|
485
|
+
const value = expression.slice(equals + 1).trim();
|
|
486
|
+
if (equals === -1 || !field || !value) throw new UsageError(`--where ${expression}: use field=value, e.g. status=preprint`, "add");
|
|
487
|
+
const current = where[field];
|
|
488
|
+
where[field] = current === void 0 ? value : [current].flat().concat(value);
|
|
489
|
+
}
|
|
490
|
+
return {
|
|
491
|
+
...Object.keys(where).length ? { where } : {},
|
|
492
|
+
since: number("since"),
|
|
493
|
+
until: number("until"),
|
|
494
|
+
sort: values.sort,
|
|
495
|
+
order: values.order,
|
|
496
|
+
limit: number("limit"),
|
|
497
|
+
groupBy: values["group-by"]
|
|
498
|
+
};
|
|
499
|
+
}
|
|
500
|
+
async function askListSettings(context, statuses) {
|
|
501
|
+
const prompt = await import('../chunks/prompt.mjs');
|
|
502
|
+
const { io } = context;
|
|
503
|
+
const settings = {};
|
|
504
|
+
if (await prompt.choose(io, "Which publications?", [{ label: "all of them", value: false }, { label: "only some statuses", value: true }])) {
|
|
505
|
+
const answer = await prompt.input(io, `Which, separated by commas (${statuses.join(", ")})`);
|
|
506
|
+
const chosen = answer.split(",").map((status) => status.trim()).filter(Boolean);
|
|
507
|
+
if (chosen.length) settings.where = { status: chosen.length === 1 ? chosen[0] : chosen };
|
|
508
|
+
}
|
|
509
|
+
const limit = await prompt.input(io, "How many at most? (Enter for all)");
|
|
510
|
+
if (limit) settings.limit = Number(limit);
|
|
511
|
+
const order = await prompt.choose(io, "In what order?", [
|
|
512
|
+
{ label: "newest first", value: {} },
|
|
513
|
+
{ label: "oldest first", value: { order: "asc" } },
|
|
514
|
+
{ label: "by title", value: { sort: "title" } },
|
|
515
|
+
{ label: "as in publications.bib", value: { sort: "source" } }
|
|
516
|
+
]);
|
|
517
|
+
Object.assign(settings, order);
|
|
518
|
+
const groupBy = await prompt.choose(io, "In groups, each with a heading?", [
|
|
519
|
+
{ label: "no, one list", value: void 0 },
|
|
520
|
+
{ label: "by status (Published, Preprints, \u2026)", value: "status" },
|
|
521
|
+
{ label: "by year", value: "year" }
|
|
522
|
+
]);
|
|
523
|
+
if (groupBy) settings.groupBy = groupBy;
|
|
524
|
+
return settings;
|
|
525
|
+
}
|
|
526
|
+
async function addPublicationList(context, name, values) {
|
|
527
|
+
const { io, run, root } = context;
|
|
528
|
+
const { inventory } = await import('../chunks/inventory.mjs');
|
|
529
|
+
const { Workspace } = await import('../chunks/plan.mjs');
|
|
530
|
+
const { loadPublicationsSchema } = await import('../chunks/bibliography.mjs').then(function (n) { return n.e; });
|
|
531
|
+
const { LISTS_FILE, LISTS_HEADER, resolveList } = await import('../chunks/publication-lists.mjs');
|
|
532
|
+
const { addPublicationList: addToFile, publicationListLine } = await import('../chunks/yaml.mjs');
|
|
533
|
+
const authoring = await import('../chunks/authoring.mjs');
|
|
534
|
+
const prompt = await import('../chunks/prompt.mjs');
|
|
535
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) throw new CommandError(`"${name}" must be lowercase kebab-case, e.g. recent-papers`);
|
|
536
|
+
const inv = await inventory(root);
|
|
537
|
+
const ws = new Workspace(root);
|
|
538
|
+
const interactive = asking(context, values);
|
|
539
|
+
const selecting = SELECTION_OPTIONS.some((option) => values[option] !== void 0);
|
|
540
|
+
const notes = [];
|
|
541
|
+
const source = await ws.read(LISTS_FILE) ?? "";
|
|
542
|
+
const line = publicationListLine(source, name);
|
|
543
|
+
const exists = line !== void 0;
|
|
544
|
+
if (exists) {
|
|
545
|
+
if (selecting) throw new CommandError(`${LISTS_FILE}:${line} declares ${name} already; change its settings there. Without selection options, this command only shows it somewhere else.`);
|
|
546
|
+
ws.keep(LISTS_FILE, `the list ${name}, as it is`);
|
|
547
|
+
} else {
|
|
548
|
+
const schema = await loadPublicationsSchema(root);
|
|
549
|
+
const settings = selecting || !interactive ? listSettings(values) : await askListSettings(context, schema.statuses.map((status) => status.id));
|
|
550
|
+
const { readBibliography } = await import('../chunks/bibliography.mjs').then(function (n) { return n.e; });
|
|
551
|
+
const { selectPublications } = await import('../chunks/publication-lists.mjs');
|
|
552
|
+
let empty;
|
|
553
|
+
try {
|
|
554
|
+
const list = resolveList(name, Object.fromEntries(Object.entries(settings).filter(([, value]) => value !== void 0)), schema);
|
|
555
|
+
empty = !selectPublications(await readBibliography(join(root, "content", "publications.bib"), schema), name, list, schema).length;
|
|
556
|
+
} catch (error) {
|
|
557
|
+
throw new CommandError(error instanceof Error ? error.message : String(error));
|
|
558
|
+
}
|
|
559
|
+
if (empty) notes.push(`No publication matches ${name} yet, so it shows nothing for now; check its settings if that is a surprise.`);
|
|
560
|
+
const result = addToFile(source || LISTS_HEADER, name, settings);
|
|
561
|
+
if (!result.ok) throw new CommandError(`${LISTS_FILE} ${result.reason}`);
|
|
562
|
+
if (source) await ws.edit(LISTS_FILE, result.value, `declares the list ${name}`);
|
|
563
|
+
else ws.create(LISTS_FILE, result.value, `declares the list ${name}`, { show: true });
|
|
564
|
+
}
|
|
565
|
+
const after = values.after;
|
|
566
|
+
const pages = strings$1(values.page).map((route) => route.endsWith("/") ? route : `${route}/`);
|
|
567
|
+
const documents = strings$1(values.document);
|
|
568
|
+
let title = values.title;
|
|
569
|
+
if (interactive && !pages.length && !documents.length) {
|
|
570
|
+
const shownOn = new Set(inv.web.publications.filter((placement) => placement.list === name).map((placement) => placement.route));
|
|
571
|
+
const choices = [
|
|
572
|
+
...inv.pages.filter((page) => page.vue && !shownOn.has(page.route)).map((page) => ({ label: `${page.route} (${page.vue})`, value: page.route })),
|
|
573
|
+
{ label: "not on the website, for now", value: "" }
|
|
574
|
+
];
|
|
575
|
+
const route = await prompt.choose(io, `Show ${name} on which page of your website?`, choices, Math.max(0, choices.findIndex((choice) => choice.value === "/publications/")));
|
|
576
|
+
if (route) pages.push(route);
|
|
577
|
+
for (const document of inv.documents.filter((candidate) => !candidate.publications.some((placement) => placement.list === name))) {
|
|
578
|
+
if (await prompt.confirm(io, `
|
|
579
|
+
Include ${name} in the PDF ${document.tex}?`, false)) documents.push(document.name);
|
|
580
|
+
}
|
|
581
|
+
if ((pages.length || documents.length) && title === void 0) title = await prompt.input(io, "\nHeading", prompt.headingFor(name));
|
|
582
|
+
}
|
|
583
|
+
title ??= prompt.headingFor(name);
|
|
584
|
+
for (const route of pages) {
|
|
585
|
+
const path = inv.pages.find((page) => page.route === route)?.vue;
|
|
586
|
+
if (!path) throw new CommandError(`--page ${route}: no Vue page has this route; \`${run.bin("vitakit", "add page <name>")}\` creates one`);
|
|
587
|
+
await authoring.planListOnPage(ws, inv, path, { list: name, ...title ? { title } : {}, ...after ? { after } : {} });
|
|
588
|
+
}
|
|
589
|
+
for (const document of documents) {
|
|
590
|
+
if (!inv.documents.some((candidate) => candidate.name === document)) {
|
|
591
|
+
throw new CommandError(`--document ${document}: no cv-tex/${document}.tex; the documents are ${inv.documents.map((candidate) => candidate.name).join(", ") || "(none)"}`);
|
|
592
|
+
}
|
|
593
|
+
await authoring.planListInDocument(ws, document, { id: name, ...title ? { title } : {}, ...after ? { after } : {} });
|
|
594
|
+
}
|
|
595
|
+
if (!pages.length && !documents.length && !exists) {
|
|
596
|
+
notes.push(`Not shown anywhere yet: \`${run.bin("vitakit", `add publication-list ${name} --page /publications/`)}\` or \`--document cv\` adds it later.`);
|
|
597
|
+
}
|
|
598
|
+
return finish(context, ws, values, {
|
|
599
|
+
before: notes,
|
|
600
|
+
after: [
|
|
601
|
+
`Which publications the list ${name} shows is set in ${LISTS_FILE}.`,
|
|
602
|
+
`\`${run.bin("vitakit", `explain ${name}`)}\` shows which ones it contains, and why.`
|
|
603
|
+
]
|
|
604
|
+
});
|
|
605
|
+
}
|
|
471
606
|
async function addPage(context, name, values) {
|
|
472
607
|
const { io, run, root } = context;
|
|
473
608
|
const { inventory } = await import('../chunks/inventory.mjs');
|
|
@@ -490,13 +625,20 @@ async function addPage(context, name, values) {
|
|
|
490
625
|
}
|
|
491
626
|
const add = {
|
|
492
627
|
name: "add",
|
|
493
|
-
summary: "add a section, a field of a section, or a
|
|
628
|
+
summary: "add a section, a field of a section, a website page, or a publication list",
|
|
494
629
|
positionals: [{ name: "what", required: true }, { name: "name" }, { name: "field" }],
|
|
495
630
|
options: {
|
|
496
|
-
"page": { type: "string", multiple: true, value: "<route>", description: "section: show it on this page, e.g. /cv/ (repeatable)" },
|
|
497
|
-
"document": { type: "string", multiple: true, value: "<name>", description: "section: include it in this PDF document, e.g. cv (repeatable)" },
|
|
498
|
-
"after": { type: "string", value: "<
|
|
499
|
-
"title": { type: "string", value: "<text>", description: "section: its heading; page: its title" },
|
|
631
|
+
"page": { type: "string", multiple: true, value: "<route>", description: "section, publication-list: show it on this page, e.g. /cv/ (repeatable)" },
|
|
632
|
+
"document": { type: "string", multiple: true, value: "<name>", description: "section, publication-list: include it in this PDF document, e.g. cv (repeatable)" },
|
|
633
|
+
"after": { type: "string", value: "<name>", description: "section, publication-list: place it after this section or list" },
|
|
634
|
+
"title": { type: "string", value: "<text>", description: "section, publication-list: its heading; page: its title" },
|
|
635
|
+
"where": { type: "string", multiple: true, value: "<field=value>", description: "publication-list: only entries with this value, e.g. status=preprint (repeatable)" },
|
|
636
|
+
"since": { type: "string", value: "<year>", description: "publication-list: only entries from this year on" },
|
|
637
|
+
"until": { type: "string", value: "<year>", description: "publication-list: only entries up to this year" },
|
|
638
|
+
"sort": { type: "string", value: "<by>", description: "publication-list: date (default), year, title, source or a field" },
|
|
639
|
+
"order": { type: "string", value: "<asc|desc>", description: "publication-list: sort direction" },
|
|
640
|
+
"group-by": { type: "string", value: "<by>", description: "publication-list: status, year or a field" },
|
|
641
|
+
"limit": { type: "string", value: "<count>", description: "publication-list: at most this many" },
|
|
500
642
|
"like": { type: "string", value: "<section>", description: "section: use the schema of another section" },
|
|
501
643
|
"type": { type: "string", value: "<type>", description: "field: text, formatted-text, number, yes-no, date, web-address, link or choice" },
|
|
502
644
|
"several": { type: "boolean", description: "field: an entry may have a list of values" },
|
|
@@ -510,7 +652,8 @@ const add = {
|
|
|
510
652
|
usage: (run) => [
|
|
511
653
|
run.bin("vitakit", "add section <name> [options]"),
|
|
512
654
|
run.bin("vitakit", "add field <section> <name> [options]"),
|
|
513
|
-
run.bin("vitakit", "add page <name> [options]")
|
|
655
|
+
run.bin("vitakit", "add page <name> [options]"),
|
|
656
|
+
run.bin("vitakit", "add publication-list <name> [options]")
|
|
514
657
|
],
|
|
515
658
|
help: (run) => `
|
|
516
659
|
A section is a file content/<name>.yml. \`add section\` creates what is missing:
|
|
@@ -533,10 +676,24 @@ content/pages/<name>.md, and adds it to the menu:
|
|
|
533
676
|
|
|
534
677
|
${run.bin("vitakit", "add page talks --title Talks")}
|
|
535
678
|
|
|
679
|
+
\`add publication-list\` declares which publications a list shows, in
|
|
680
|
+
content/publication-lists.yml, and shows it on a page or in a PDF:
|
|
681
|
+
|
|
682
|
+
${run.bin("vitakit", "add publication-list preprints --where status=preprint --page /publications/")}
|
|
683
|
+
${run.bin("vitakit", 'add publication-list recent --limit 5 --document cv --title "Recent publications"')}
|
|
684
|
+
|
|
685
|
+
For a list that exists already, it only adds it to more pages and PDFs.
|
|
686
|
+
|
|
536
687
|
Everything is shown before it is written, and asked; nothing existing is
|
|
537
688
|
replaced. A file that cannot be changed safely gets a line to paste instead.`,
|
|
538
689
|
async run(context, { positionals, values }) {
|
|
539
690
|
const [what, name, field] = positionals;
|
|
691
|
+
if (what === "publication-list") {
|
|
692
|
+
if (!name) throw new UsageError("missing <name>: `add publication-list <name>`", "add");
|
|
693
|
+
if (field !== void 0) throw new UsageError(`unexpected argument "${field}"`, "add");
|
|
694
|
+
onlyOptions(what, values, LIST_OPTIONS);
|
|
695
|
+
return addPublicationList(context, name, values);
|
|
696
|
+
}
|
|
540
697
|
if (what === "section" || what === "page") {
|
|
541
698
|
if (!name) throw new UsageError(`missing <name>: \`add ${what} <name>\``, "add");
|
|
542
699
|
if (field !== void 0) throw new UsageError(`unexpected argument "${field}"`, "add");
|
|
@@ -548,7 +705,7 @@ replaced. A file that cannot be changed safely gets a line to paste instead.`,
|
|
|
548
705
|
onlyOptions(what, values, FIELD_OPTIONS);
|
|
549
706
|
return addField(context, name, field, values);
|
|
550
707
|
}
|
|
551
|
-
throw new UsageError(`add what? \`add section <name>\`, \`add field <section> <name>\` or \`add
|
|
708
|
+
throw new UsageError(`add what? \`add section <name>\`, \`add field <section> <name>\`, \`add page <name>\` or \`add publication-list <name>\`${what ? `, not "${what}"` : ""}`, "add");
|
|
552
709
|
}
|
|
553
710
|
};
|
|
554
711
|
|
|
@@ -562,12 +719,12 @@ async function planSchema(context, ws, inv, id, values, notes) {
|
|
|
562
719
|
const path = `schemas/${id}.ts`;
|
|
563
720
|
if (id === "publications") {
|
|
564
721
|
if (ws.exists(path)) {
|
|
565
|
-
notes.after.push(`${path} is yours already: its
|
|
722
|
+
notes.after.push(`${path} is yours already: its statuses, badges and fields decide what publications can be and how they are linked. Which ones a list shows is set in content/publication-lists.yml.`);
|
|
566
723
|
return;
|
|
567
724
|
}
|
|
568
|
-
ws.create(path, await builtinSchemaSource("publications"), "vitakit's publication
|
|
725
|
+
ws.create(path, await builtinSchemaSource("publications"), "vitakit's publication statuses, badges and fields, to change");
|
|
569
726
|
notes.before.push("The website, `vitakit verify` and the PDF all read it from now on.");
|
|
570
|
-
notes.after.push(`Edit ${path}: change
|
|
727
|
+
notes.after.push(`Edit ${path}: change statuses, add badges, pass on BibTeX fields. Check it: \`${run.bin("vitakit", "validate")}\``);
|
|
571
728
|
return;
|
|
572
729
|
}
|
|
573
730
|
const section = inv.sections.find((candidate) => candidate.id === id);
|
|
@@ -596,7 +753,7 @@ async function planSchema(context, ws, inv, id, values, notes) {
|
|
|
596
753
|
async function planWeb(context, ws, inv, section, fields, list, values, notes) {
|
|
597
754
|
const { io, run } = context;
|
|
598
755
|
const { entryComponent, listComponent } = await import('../chunks/snippets.mjs');
|
|
599
|
-
const { useComponentForSection } = await import('../chunks/vue.mjs')
|
|
756
|
+
const { useComponentForSection } = await import('../chunks/vue.mjs');
|
|
600
757
|
const { planPlaceOnPage } = await import('../chunks/authoring.mjs');
|
|
601
758
|
const prompt = await import('../chunks/prompt.mjs');
|
|
602
759
|
const id = section.id;
|
|
@@ -737,10 +894,12 @@ ${copied}`;
|
|
|
737
894
|
}
|
|
738
895
|
if (ws.exists(path)) ws.replace(path, source, `${reason}; yours is kept next to it as a .backup`, { show: true });
|
|
739
896
|
else ws.create(path, source, reason, { show: true });
|
|
740
|
-
const included = inv.documents.filter((document) => document.placements.some((placement) => placement.section === id));
|
|
741
|
-
if (!included.length)
|
|
897
|
+
const included = inv.documents.filter((document) => id === "publications" ? document.publications.length : document.placements.some((placement) => placement.section === id));
|
|
898
|
+
if (!included.length) {
|
|
899
|
+
notes.before.push(id === "publications" ? `No PDF document shows a publication list yet: add \\vitapublications[Publications]{all} to ${inv.documents[0].tex}` : `No PDF document includes ${id} yet: \`${run.bin("vitakit", `add section ${id} --document ${inv.documents[0].name}`)}\``);
|
|
900
|
+
}
|
|
742
901
|
if (id === "publications") {
|
|
743
|
-
notes.before.push("This template
|
|
902
|
+
notes.before.push("This template prints a publication list, group by group; how each reference is formatted is up to biblatex, in cv-tex/preamble.tex.");
|
|
744
903
|
}
|
|
745
904
|
notes.after.push(`Edit ${path}; \`${run.bin("vitakit", `validate ${id}`)}\` checks it without LaTeX, \`${run.bin("vitakit", "pdf")}\` builds.`);
|
|
746
905
|
}
|
|
@@ -937,6 +1096,12 @@ function pdfCell(inventory, section) {
|
|
|
937
1096
|
const documents = [...new Set(pdfPlacementsOf(inventory, section).map((placement) => placement.document))];
|
|
938
1097
|
return documents.length ? documents.join(", ") : "\u2013";
|
|
939
1098
|
}
|
|
1099
|
+
function listPlacementsText(inventory, list) {
|
|
1100
|
+
const pages = inventory.web.publications.filter((placement) => placement.list === list).map((placement) => placement.route ?? placement.file);
|
|
1101
|
+
const documents = inventory.documents.filter((document) => document.publications.some((placement) => placement.list === list)).map((document) => `${document.name} (pdf)`);
|
|
1102
|
+
const all = [.../* @__PURE__ */ new Set([...pages, ...documents])];
|
|
1103
|
+
return all.length ? all.join(", ") : "not shown anywhere";
|
|
1104
|
+
}
|
|
940
1105
|
function readableIssue(line) {
|
|
941
1106
|
return line.replace(/^(\s*)- (\d+)(?:\.([\w.-]+))?: /, (_match, indent, index, field) => `${indent}entry ${Number(index) + 1}${field ? `, ${field}` : ""}: `).replace(/^(\s*)- /, "$1");
|
|
942
1107
|
}
|
|
@@ -970,6 +1135,7 @@ const describe = {
|
|
|
970
1135
|
__proto__: null,
|
|
971
1136
|
countsText: countsText,
|
|
972
1137
|
firstLine: firstLine,
|
|
1138
|
+
listPlacementsText: listPlacementsText,
|
|
973
1139
|
messageLines: messageLines,
|
|
974
1140
|
pdfCell: pdfCell,
|
|
975
1141
|
pdfPlacementsOf: pdfPlacementsOf,
|
|
@@ -1000,7 +1166,7 @@ function schemaText(section) {
|
|
|
1000
1166
|
async function fieldSnippets(context, inv, section, names) {
|
|
1001
1167
|
const { style, run, root } = context;
|
|
1002
1168
|
const { webFieldSnippet, etaFieldSnippet } = await import('../chunks/snippets.mjs');
|
|
1003
|
-
const { existsSync } = await import('node:fs');
|
|
1169
|
+
const { existsSync: existsSync2 } = await import('node:fs');
|
|
1004
1170
|
const { join: join2 } = await import('node:path');
|
|
1005
1171
|
const id = section.id;
|
|
1006
1172
|
const unknown = names.filter((name) => !section.fields?.some((field) => field.path === name));
|
|
@@ -1031,7 +1197,7 @@ async function fieldSnippets(context, inv, section, names) {
|
|
|
1031
1197
|
if (components.length) lines.push("", ...indented(web.map((snippet) => snippet.markup)));
|
|
1032
1198
|
if (imports.length) lines.push("", ` import { ${imports.join(", ")} } from '@anja.codes/vitakit/shared'`);
|
|
1033
1199
|
lines.push("");
|
|
1034
|
-
if (
|
|
1200
|
+
if (existsSync2(join2(root, "cv-tex", "templates", `${id}.eta`))) {
|
|
1035
1201
|
lines.push(block("pdf", [[`cv-tex/templates/${id}.eta (yours): add inside its loop over the entries, after the notes`]]));
|
|
1036
1202
|
} else if (inv.documents.length) {
|
|
1037
1203
|
lines.push(block("pdf", [[`vitakit's template. \`${run.bin("vitakit", `customize ${id} --pdf --field ${names.join(" --field ")}`)}\` copies it with these lines:`]]));
|
|
@@ -1108,9 +1274,7 @@ async function explainDocument(context, inv, document) {
|
|
|
1108
1274
|
lines.push(block("includes", document.placements.map((placement) => {
|
|
1109
1275
|
const section = inv.sections.find((candidate) => candidate.id === placement.section);
|
|
1110
1276
|
let detail = "";
|
|
1111
|
-
if (
|
|
1112
|
-
detail = inv.publications ? `${inv.publications.entries} entries in ${inv.publications.groups} groups` : "no content/publications.bib";
|
|
1113
|
-
} else if (section?.problems.length) {
|
|
1277
|
+
if (section?.problems.length) {
|
|
1114
1278
|
detail = style.red("has problems");
|
|
1115
1279
|
} else if (section) {
|
|
1116
1280
|
const { total, steps } = explainSelection(inv.content.sections[section.id] ?? [], document.options, section.id);
|
|
@@ -1128,12 +1292,69 @@ async function explainDocument(context, inv, document) {
|
|
|
1128
1292
|
} else {
|
|
1129
1293
|
lines.push(block("includes", [["no \\vitasection or \\vitainput lines found"]]));
|
|
1130
1294
|
}
|
|
1295
|
+
if (document.publications.length) {
|
|
1296
|
+
lines.push("", block("lists", document.publications.map((placement) => [
|
|
1297
|
+
`line ${placement.line}`,
|
|
1298
|
+
placement.list,
|
|
1299
|
+
placement.heading ?? "",
|
|
1300
|
+
inv.publications?.counts[placement.list] !== void 0 ? entriesText(inv.publications.counts[placement.list]) : style.red("no such list")
|
|
1301
|
+
])));
|
|
1302
|
+
}
|
|
1131
1303
|
const pdf = document.pdf ? `${document.pdf.file}${document.pdf.mtimeMs < inv.sourcesMtimeMs ? ", older than its sources" : ""}` : "not built yet";
|
|
1132
1304
|
lines.push("", block("pdf", [[pdf]]));
|
|
1133
1305
|
if (document.problems.length) lines.push("", style.bold("Problems"), ...problemLines(document.problems));
|
|
1134
1306
|
lines.push("", `Build it: \`${run.bin("vitakit", `pdf ${document.name}`)}\`; per section: \`${run.bin("vitakit", "explain <section>")}\``);
|
|
1135
1307
|
return lines;
|
|
1136
1308
|
}
|
|
1309
|
+
const entriesText = (count) => `${count} ${count === 1 ? "entry" : "entries"}`;
|
|
1310
|
+
async function explainPublications(context, inv) {
|
|
1311
|
+
const { style, run, root } = context;
|
|
1312
|
+
const { loadPublicationsSchema } = await import('../chunks/bibliography.mjs').then(function (n) { return n.e; });
|
|
1313
|
+
const { LISTS_FILE, describeList, loadPublicationLists } = await import('../chunks/publication-lists.mjs');
|
|
1314
|
+
const lines = [style.bold("publications"), ""];
|
|
1315
|
+
lines.push(block("data", [[inv.publications ? `${inv.publications.file} \xB7 ${inv.publications.entries} entries` : "no content/publications.bib"]]));
|
|
1316
|
+
lines.push(block("statuses", [[existsSync(join(root, "schemas", "publications.ts")) ? "schemas/publications.ts (yours)" : `vitakit's (\`${run.bin("vitakit", "customize publications")}\` to change them)`]]));
|
|
1317
|
+
const lists = await loadPublicationsSchema(root).then((schema) => loadPublicationLists(root, schema)).catch(() => void 0);
|
|
1318
|
+
if (!lists) {
|
|
1319
|
+
lines.push(block("lists", [[style.red(`${LISTS_FILE} has problems`)]]));
|
|
1320
|
+
} else if (!Object.keys(lists).length) {
|
|
1321
|
+
lines.push(block("lists", [[`none yet: \`${run.bin("vitakit", "add publication-list <name>")}\` declares one in ${LISTS_FILE}`]]));
|
|
1322
|
+
} else {
|
|
1323
|
+
lines.push(block("lists", [[LISTS_FILE]]));
|
|
1324
|
+
lines.push(block("", Object.entries(lists).flatMap(([name, list]) => [
|
|
1325
|
+
[` ${name}`, `${entriesText(inv.publications?.counts[name] ?? 0)} \xB7 ${describeList(list)}`],
|
|
1326
|
+
["", listPlacementsText(inv, name)]
|
|
1327
|
+
])));
|
|
1328
|
+
}
|
|
1329
|
+
const files = [LISTS_FILE, "content/publications.bib", "schemas/publications.ts"];
|
|
1330
|
+
const problems = inv.problems.filter((problem) => files.includes(problem.where));
|
|
1331
|
+
if (problems.length) lines.push("", style.bold("Problems"), ...problemLines(problems));
|
|
1332
|
+
lines.push("", `Show a list: <VitaPublications list="<name>" /> on a page, \\vitapublications[Heading]{<name>} in a PDF.`);
|
|
1333
|
+
return lines;
|
|
1334
|
+
}
|
|
1335
|
+
async function explainList(context, inv, name) {
|
|
1336
|
+
const { style, run, root } = context;
|
|
1337
|
+
const { loadPublicationsSchema, readBibliography } = await import('../chunks/bibliography.mjs').then(function (n) { return n.e; });
|
|
1338
|
+
const { LISTS_FILE, describeList, explainListSelection, loadPublicationLists } = await import('../chunks/publication-lists.mjs');
|
|
1339
|
+
const { publicationListLine } = await import('../chunks/yaml.mjs');
|
|
1340
|
+
const { readFile } = await import('node:fs/promises');
|
|
1341
|
+
const schema = await loadPublicationsSchema(root);
|
|
1342
|
+
const list = (await loadPublicationLists(root, schema))[name];
|
|
1343
|
+
const line = publicationListLine(await readFile(join(root, LISTS_FILE), "utf8"), name);
|
|
1344
|
+
const { total, steps: removed, keys } = explainListSelection(await readBibliography(join(root, "content", "publications.bib"), schema), name, list, schema);
|
|
1345
|
+
const lines = [style.bold(name), ""];
|
|
1346
|
+
lines.push(block("list", [[`${LISTS_FILE}${line ? `:${line}` : ""}`], [describeList(list)]]));
|
|
1347
|
+
lines.push(block("shows", [
|
|
1348
|
+
[`${keys.length} of ${total} publications${removed.length ? ` (${removed.map((step) => `${step.rule} \u2212${step.removed}`).join(", ")})` : ""}`],
|
|
1349
|
+
...keys.length ? [[keys.join(", ")]] : []
|
|
1350
|
+
]), "");
|
|
1351
|
+
const web = inv.web.publications.filter((placement) => placement.list === name);
|
|
1352
|
+
lines.push(block("website", web.length ? web.map((placement) => [placement.route ?? "", `${placement.file}:${placement.line}`, `<VitaPublications${placement.title !== void 0 ? ` title="${placement.title}"` : ""}>`]) : [["not on any page"]]));
|
|
1353
|
+
const pdf = inv.documents.flatMap((document) => document.publications.filter((placement) => placement.list === name));
|
|
1354
|
+
lines.push("", block("pdf", pdf.length ? pdf.map((placement) => [placement.document, `${placement.file}:${placement.line}`, `\\vitapublications${placement.heading !== void 0 ? `[${placement.heading}]` : ""}{${name}}`]) : [["not in any PDF document"]]));
|
|
1355
|
+
if (!web.length && !pdf.length) lines.push("", `Show it: \`${run.bin("vitakit", `add publication-list ${name} --page /publications/`)}\`, or --document cv.`);
|
|
1356
|
+
return lines;
|
|
1357
|
+
}
|
|
1137
1358
|
function explainRoute(context, inv, page, route) {
|
|
1138
1359
|
const { style } = context;
|
|
1139
1360
|
const lines = [style.bold(route), ""];
|
|
@@ -1152,61 +1373,131 @@ function explainRoute(context, inv, page, route) {
|
|
|
1152
1373
|
placement.component === "code" ? "useSection, in code" : `<${placement.component}${placement.title !== void 0 ? ` title="${placement.title}"` : ""}>`
|
|
1153
1374
|
])));
|
|
1154
1375
|
}
|
|
1376
|
+
const lists = inv.web.publications.filter((placement) => placement.route === route);
|
|
1377
|
+
if (lists.length) {
|
|
1378
|
+
lines.push("", block("lists", lists.map((placement) => [
|
|
1379
|
+
placement.list ?? placement.expression ?? "(no list)",
|
|
1380
|
+
`line ${placement.line}`,
|
|
1381
|
+
`<VitaPublications${placement.title !== void 0 ? ` title="${placement.title}"` : ""}>`
|
|
1382
|
+
])));
|
|
1383
|
+
}
|
|
1155
1384
|
const dynamic = inv.web.dynamic.filter((usage) => usage.file === page?.vue);
|
|
1156
1385
|
if (dynamic.length) {
|
|
1157
1386
|
lines.push("", "Chosen in code, so not listed:", ...dynamic.map((usage) => ` line ${usage.line} ${usage.usage}`));
|
|
1158
1387
|
}
|
|
1159
1388
|
return lines;
|
|
1160
1389
|
}
|
|
1390
|
+
const KINDS = ["section", "document", "publication-list", "page"];
|
|
1391
|
+
const KIND_NAMES = { "section": "section", "document": "PDF document", "publication-list": "publication list", "page": "page" };
|
|
1161
1392
|
const explain = {
|
|
1162
1393
|
name: "explain",
|
|
1163
|
-
summary: "where a section, PDF document or page comes from, and where it goes",
|
|
1164
|
-
|
|
1394
|
+
summary: "where a section, publication list, PDF document or page comes from, and where it goes",
|
|
1395
|
+
// `<name>`, or `<kind> <name>`; the usage lines say so.
|
|
1396
|
+
positionals: [{ name: "name", required: true }, { name: "name" }],
|
|
1397
|
+
usage: (run) => [
|
|
1398
|
+
run.bin("vitakit", "explain <name> [options]"),
|
|
1399
|
+
run.bin("vitakit", "explain <kind> <name> [options]")
|
|
1400
|
+
],
|
|
1165
1401
|
options: {
|
|
1166
|
-
section: { type: "boolean", description: "the name is a section (when a document has the same name)" },
|
|
1167
|
-
document: { type: "boolean", description: "the name is a PDF document (when a section has the same name)" },
|
|
1168
1402
|
field: { type: "string", multiple: true, value: "<name>", description: "a section: lines that show this field, on the website and in the PDF" }
|
|
1169
1403
|
},
|
|
1170
1404
|
project: true,
|
|
1171
1405
|
help: (run) => `
|
|
1172
|
-
${run.bin("vitakit", "explain talks")}
|
|
1173
|
-
${run.bin("vitakit", "explain cv")}
|
|
1174
|
-
${run.bin("vitakit", "explain /cv/")}
|
|
1406
|
+
${run.bin("vitakit", "explain talks")} a section: its file, fields, pages and PDFs
|
|
1407
|
+
${run.bin("vitakit", "explain cv")} a PDF document: what it includes, from where
|
|
1408
|
+
${run.bin("vitakit", "explain /cv/")} a website page: its files, sections and lists
|
|
1409
|
+
${run.bin("vitakit", "explain recent")} a publication list: which publications, and why
|
|
1410
|
+
${run.bin("vitakit", "explain publications")} every publication list: what it shows, and where
|
|
1175
1411
|
${run.bin("vitakit", "explain teaching --field evaluation")} lines that show a field
|
|
1176
1412
|
|
|
1413
|
+
A name is enough while it is unique. To say what it is -- when a section, a
|
|
1414
|
+
document and a list share a name, or in a script -- put the kind first, as
|
|
1415
|
+
\`add\` does: ${KINDS.join(", ")}.
|
|
1416
|
+
|
|
1417
|
+
${run.bin("vitakit", "explain section talks")}
|
|
1418
|
+
${run.bin("vitakit", "explain publication-list talks")}
|
|
1419
|
+
${run.bin("vitakit", "explain page cv")}
|
|
1420
|
+
|
|
1177
1421
|
For a section it shows which entries each PDF document selects, and which
|
|
1178
1422
|
setting removed the others -- the answer to "why is this not in my PDF?".
|
|
1179
|
-
|
|
1423
|
+
For a publication list, the same: which setting removed which publications,
|
|
1424
|
+
and the citation keys of those it shows. Entry contents are never printed.`,
|
|
1180
1425
|
async run(context, { positionals, values }) {
|
|
1181
1426
|
const { inventory } = await import('../chunks/inventory.mjs');
|
|
1182
1427
|
const inv = await inventory(context.root);
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
} else {
|
|
1189
|
-
const section = inv.sections.find((candidate) => candidate.id === name);
|
|
1190
|
-
const document = inv.documents.find((candidate) => candidate.name === name);
|
|
1191
|
-
if (values.section && values.document) throw new UsageError("use --section or --document, not both", "explain");
|
|
1192
|
-
if (section && document && !values.section && !values.document) {
|
|
1193
|
-
throw new UsageError(`"${name}" is both a section and a PDF document; add --section or --document`, "explain");
|
|
1194
|
-
}
|
|
1195
|
-
const fields = values.field ?? [];
|
|
1196
|
-
if (fields.length && !(section && !values.document)) throw new UsageError("--field goes with a section", "explain");
|
|
1197
|
-
if (section && !values.document) lines = await explainSection(context, inv, section, fields);
|
|
1198
|
-
else if (document && !values.section) lines = await explainDocument(context, inv, document);
|
|
1199
|
-
else {
|
|
1200
|
-
const known = [
|
|
1201
|
-
inv.sections.length ? `sections: ${inv.sections.map((candidate) => candidate.id).join(", ")}` : "",
|
|
1202
|
-
inv.documents.length ? `documents: ${inv.documents.map((candidate) => candidate.name).join(", ")}` : ""
|
|
1203
|
-
].filter(Boolean).join("; ");
|
|
1204
|
-
throw new CommandError(`nothing called "${name}"${known ? `; ${known}` : ""}. A page is explained by its route, e.g. /cv/.`);
|
|
1205
|
-
}
|
|
1428
|
+
const typed = positionals.length === 2;
|
|
1429
|
+
const kind = typed ? positionals[0] : void 0;
|
|
1430
|
+
const name = typed ? positionals[1] : positionals[0];
|
|
1431
|
+
if (kind !== void 0 && !KINDS.includes(kind)) {
|
|
1432
|
+
throw new UsageError(`"${kind}" is not a kind: \`explain <kind> <name>\` takes ${KINDS.join(", ")}; or a name alone`, "explain");
|
|
1206
1433
|
}
|
|
1207
|
-
|
|
1434
|
+
const fields = values.field ?? [];
|
|
1435
|
+
const out = (lines) => {
|
|
1436
|
+
context.io.out(`${lines.join("\n")}
|
|
1208
1437
|
`);
|
|
1209
|
-
|
|
1438
|
+
return 0;
|
|
1439
|
+
};
|
|
1440
|
+
if (kind === "page" || !kind && name.startsWith("/")) {
|
|
1441
|
+
const route = name === "/" || name === "index" ? "/" : `/${name.replace(/^\/|\/$/g, "")}/`;
|
|
1442
|
+
return out(explainRoute(context, inv, inv.pages.find((page) => page.route === route), route));
|
|
1443
|
+
}
|
|
1444
|
+
if (!kind && name === "publications") return out(await explainPublications(context, inv));
|
|
1445
|
+
const lists = Object.keys(inv.content.publications);
|
|
1446
|
+
const section = inv.sections.find((candidate) => candidate.id === name);
|
|
1447
|
+
const document = inv.documents.find((candidate) => candidate.name === name);
|
|
1448
|
+
const found = [
|
|
1449
|
+
...section ? [{ kind: "section", explain: () => explainSection(context, inv, section, fields) }] : [],
|
|
1450
|
+
...document ? [{ kind: "document", explain: () => explainDocument(context, inv, document) }] : [],
|
|
1451
|
+
...lists.includes(name) ? [{ kind: "publication-list", explain: () => explainList(context, inv, name) }] : []
|
|
1452
|
+
];
|
|
1453
|
+
const chosen = kind ? found.filter((candidate) => candidate.kind === kind) : found;
|
|
1454
|
+
if (chosen.length > 1) {
|
|
1455
|
+
throw new UsageError([
|
|
1456
|
+
`"${name}" is ${chosen.map((candidate) => `a ${KIND_NAMES[candidate.kind]}`).join(" and ")}; say which:`,
|
|
1457
|
+
...chosen.map((candidate) => ` ${context.run.bin("vitakit", `explain ${candidate.kind} ${name}`)}`)
|
|
1458
|
+
].join("\n"), "explain");
|
|
1459
|
+
}
|
|
1460
|
+
if (chosen.length === 1) {
|
|
1461
|
+
if (fields.length && chosen[0].kind !== "section") throw new UsageError("--field goes with a section", "explain");
|
|
1462
|
+
return out(await chosen[0].explain());
|
|
1463
|
+
}
|
|
1464
|
+
if (!kind && KINDS.includes(name)) throw new UsageError(`missing <name>: \`explain ${name} <name>\``, "explain");
|
|
1465
|
+
const known = [
|
|
1466
|
+
inv.sections.length ? `sections: ${inv.sections.map((candidate) => candidate.id).join(", ")}` : "",
|
|
1467
|
+
inv.documents.length ? `documents: ${inv.documents.map((candidate) => candidate.name).join(", ")}` : "",
|
|
1468
|
+
lists.length ? `publication lists: ${lists.join(", ")}` : ""
|
|
1469
|
+
].filter(Boolean).join("; ");
|
|
1470
|
+
throw new CommandError(`${kind ? `no ${KIND_NAMES[kind]}` : "nothing"} called "${name}"${known ? `; ${known}` : ""}. A page is explained by its route, e.g. /cv/.`);
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
|
|
1474
|
+
const migrate = {
|
|
1475
|
+
name: "migrate",
|
|
1476
|
+
summary: "update a project written for an earlier vitakit",
|
|
1477
|
+
positionals: [],
|
|
1478
|
+
options: {
|
|
1479
|
+
"dry-run": { type: "boolean", description: "show what would be written, write nothing" },
|
|
1480
|
+
"yes": { type: "boolean", description: "ask nothing and write" }
|
|
1481
|
+
},
|
|
1482
|
+
project: true,
|
|
1483
|
+
help: (run) => `
|
|
1484
|
+
After updating vitakit, changes your files where they use what it no longer
|
|
1485
|
+
supports. Everything is shown before it is written, and asked; what cannot be
|
|
1486
|
+
changed safely is listed for you to do by hand. Running it again changes
|
|
1487
|
+
nothing that is up to date.
|
|
1488
|
+
|
|
1489
|
+
${run.bin("vitakit", "migrate --dry-run")}
|
|
1490
|
+
${run.bin("vitakit", "migrate")}
|
|
1491
|
+
|
|
1492
|
+
Then check the project: \`${run.bin("vitakit", "validate")}\``,
|
|
1493
|
+
async run(context, { values }) {
|
|
1494
|
+
const { Workspace } = await import('../chunks/plan.mjs');
|
|
1495
|
+
const { migratePublicationLists } = await import('../chunks/migrations.mjs');
|
|
1496
|
+
const ws = new Workspace(context.root);
|
|
1497
|
+
const notes = { before: [], after: [] };
|
|
1498
|
+
await migratePublicationLists(context.root, ws, notes);
|
|
1499
|
+
notes.after.push(`Check the project: \`${context.run.bin("vitakit", "validate")}\``);
|
|
1500
|
+
return finish(context, ws, values, notes);
|
|
1210
1501
|
}
|
|
1211
1502
|
};
|
|
1212
1503
|
|
|
@@ -1278,6 +1569,7 @@ Reads your files only: no website build, no LaTeX. Entry contents are never
|
|
|
1278
1569
|
printed, only how many there are.
|
|
1279
1570
|
|
|
1280
1571
|
Next: \`${run.bin("vitakit", "explain <section>")}\` for the details of one section,
|
|
1572
|
+
\`${run.bin("vitakit", "explain publications")}\` for the publication lists,
|
|
1281
1573
|
\`${run.bin("vitakit", "validate")}\` to check everything before building.`,
|
|
1282
1574
|
async run({ io, style, run, root, json }) {
|
|
1283
1575
|
const { inventory } = await import('../chunks/inventory.mjs');
|
|
@@ -1324,7 +1616,11 @@ Next: \`${run.bin("vitakit", "explain <section>")}\` for the details of one sect
|
|
|
1324
1616
|
}
|
|
1325
1617
|
const facts = [];
|
|
1326
1618
|
if (inv.publications) {
|
|
1327
|
-
|
|
1619
|
+
const { counts } = inv.publications;
|
|
1620
|
+
facts.push([style.bold("Publications"), `${inv.publications.file} \xB7 ${inv.publications.entries} entries${Object.keys(counts).length ? "" : " \xB7 no lists yet (content/publication-lists.yml)"}`]);
|
|
1621
|
+
for (const [name, count2] of Object.entries(counts)) {
|
|
1622
|
+
facts.push(["", ` ${name}: ${count2} ${count2 === 1 ? "entry" : "entries"} \xB7 ${listPlacementsText(inv, name)}`]);
|
|
1623
|
+
}
|
|
1328
1624
|
}
|
|
1329
1625
|
const pages = inv.pages.filter((page) => page.markdown || page.vue);
|
|
1330
1626
|
if (pages.length) {
|
|
@@ -1464,11 +1760,11 @@ ${style.green("The generated site looks correct.")}
|
|
|
1464
1760
|
}
|
|
1465
1761
|
};
|
|
1466
1762
|
|
|
1467
|
-
const COMMANDS = [status, explain, validate, add, customize, pdf, verify, eject];
|
|
1763
|
+
const COMMANDS = [status, explain, validate, add, customize, pdf, verify, eject, migrate];
|
|
1468
1764
|
const GROUPS = [
|
|
1469
1765
|
{ title: "Look", names: ["status", "explain", "validate"] },
|
|
1470
1766
|
{ title: "Build and check", names: ["pdf", "verify"] },
|
|
1471
|
-
{ title: "Change", names: ["add", "customize", "eject"] }
|
|
1767
|
+
{ title: "Change", names: ["add", "customize", "eject", "migrate"] }
|
|
1472
1768
|
];
|
|
1473
1769
|
|
|
1474
1770
|
function version() {
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
<% if (it.groups.length) { %>
|
|
2
2
|
\begin{refsection}
|
|
3
|
-
\
|
|
3
|
+
\begin{refcontext}[sorting=none]
|
|
4
|
+
<%~ it.bib.cite() %>
|
|
4
5
|
<% for (const group of it.groups) { %>
|
|
5
6
|
<%~ it.bib.group(group) %>
|
|
6
|
-
\printbibliography[check=vita-<%~ group.id %>, title={<%~ it.text(group.label) %>}
|
|
7
|
+
\printbibliography[resetnumbers=true, check=vita-<%~ group.id %>, <% if (group.label !== undefined) { %>heading=subbibliography, title={<%~ it.text(group.label) %>}<% } else { %>heading=none<% } %>]
|
|
7
8
|
<% } %>
|
|
9
|
+
\end{refcontext}
|
|
8
10
|
\end{refsection}
|
|
9
11
|
<% } %>
|
package/dist/tex/vitakit-cv.cls
CHANGED
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
\RequirePackage[backend=biber, style=numeric, sorting=ydnt,
|
|
37
37
|
defernumbers=true, maxbibnames=99, giveninits=true]{biblatex}
|
|
38
38
|
%% \vitabibgroup{id}{key,key,...} defines the bibliography check `vita-id`,
|
|
39
|
-
%% which keeps exactly the listed entries.
|
|
40
|
-
%%
|
|
41
|
-
%%
|
|
42
|
-
%%
|
|
39
|
+
%% which keeps exactly the listed entries. A generated list cites its keys
|
|
40
|
+
%% in the list's order and prints one bibliography per group with
|
|
41
|
+
%% sorting=none, so the PDF lists what the website lists, in its order.
|
|
42
|
+
%% Keys are compared as strings.
|
|
43
43
|
\newcommand{\vitabibgroup}[2]{%
|
|
44
44
|
\defbibcheck{vita-#1}{%
|
|
45
45
|
\edef\vita@bibkey{\thefield{entrykey}}%
|
|
@@ -131,4 +131,15 @@
|
|
|
131
131
|
%% for sections that bring their own structure (publications).
|
|
132
132
|
\newcommand{\vitainput}[1]{\input{generated/\vita@document/#1.tex}}
|
|
133
133
|
|
|
134
|
+
%% Generated profile.tex marks publication lists without entries.
|
|
135
|
+
\newcommand{\vitaemptypublications}[1]{\expandafter\def\csname vita@emptylist@#1\endcsname{}}
|
|
136
|
+
|
|
137
|
+
%% \vitapublications[Heading]{list} -- a list from content/publication-lists.yml,
|
|
138
|
+
%% with an optional heading; neither appears when the list has no entries.
|
|
139
|
+
\NewDocumentCommand{\vitapublications}{o m}{%
|
|
140
|
+
\ifcsname vita@emptylist@#2\endcsname\else
|
|
141
|
+
\IfNoValueF{#1}{\section{#1}}%
|
|
142
|
+
\input{generated/\vita@document/publications/#2.tex}%
|
|
143
|
+
\fi}
|
|
144
|
+
|
|
134
145
|
\endinput
|