@anja.codes/vitakit 0.1.0 → 0.2.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 (63) hide show
  1. package/README.md +51 -131
  2. package/dist/check.mjs +12 -5
  3. package/dist/chunks/authoring.mjs +367 -0
  4. package/dist/chunks/build.mjs +176 -0
  5. package/dist/chunks/documents.mjs +143 -0
  6. package/dist/chunks/draft.mjs +222 -0
  7. package/dist/chunks/eject.mjs +59 -0
  8. package/dist/chunks/emit.mjs +200 -0
  9. package/dist/chunks/eta.mjs +16 -0
  10. package/dist/chunks/fields.mjs +79 -0
  11. package/dist/chunks/inventory.mjs +199 -0
  12. package/dist/chunks/plan.mjs +144 -0
  13. package/dist/chunks/prompt.mjs +33 -0
  14. package/dist/chunks/schema-source.mjs +12 -0
  15. package/dist/chunks/schemas.mjs +109 -0
  16. package/dist/chunks/sections.mjs +109 -0
  17. package/dist/chunks/snippets.mjs +199 -0
  18. package/dist/chunks/validate.mjs +124 -0
  19. package/dist/chunks/vue.mjs +116 -0
  20. package/dist/cli.d.mts +33 -40
  21. package/dist/cli.mjs +7 -224
  22. package/dist/index.d.mts +372 -42
  23. package/dist/index.mjs +10 -4
  24. package/dist/nuxt.mjs +35 -5
  25. package/dist/runtime/components/VitaSection.vue +94 -38
  26. package/dist/schemas/education.ts +4 -4
  27. package/dist/schemas/employment.ts +3 -3
  28. package/dist/schemas/publications.ts +2 -2
  29. package/dist/schemas/supervision.ts +3 -3
  30. package/dist/schemas/talks.ts +1 -1
  31. package/dist/schemas/teaching.ts +3 -3
  32. package/dist/shared/index.d.mts +60 -9
  33. package/dist/shared/index.mjs +3 -57
  34. package/dist/shared/vitakit.BZZIsWlY.mjs +179 -0
  35. package/dist/shared/vitakit.BerUaxtF.mjs +121 -0
  36. package/dist/shared/{vitakit.D8WRg_Xx.d.mts → vitakit.BfpxDtz1.d.mts} +9 -8
  37. package/dist/shared/{vitakit.CypCF-4Q.mjs → vitakit.By5VWQ1n.mjs} +12 -78
  38. package/dist/shared/vitakit.CfB3hMHl.mjs +60 -0
  39. package/dist/shared/vitakit.CoWX2Z4R.mjs +1554 -0
  40. package/dist/shared/{vitakit.BlU-p01e.mjs → vitakit.CtChKO0I.mjs} +1 -1
  41. package/dist/shared/vitakit.D-DdJaDZ.mjs +20 -0
  42. package/dist/shared/vitakit.D10HyDhF.mjs +19 -0
  43. package/dist/shared/vitakit.DQ6PmGoL.mjs +34 -0
  44. package/dist/shared/vitakit.DbSeENVS.mjs +58 -0
  45. package/dist/shared/vitakit.DcQOdM4I.mjs +57 -0
  46. package/dist/shared/vitakit.Do5HC_MD.mjs +142 -0
  47. package/dist/shared/vitakit.TSMTC5Jl.mjs +6 -0
  48. package/dist/tex/templates/_generic.eta +6 -4
  49. package/dist/tex/templates/education.eta +10 -5
  50. package/dist/tex/templates/employment.eta +12 -5
  51. package/dist/tex/templates/publications.eta +4 -2
  52. package/dist/tex/templates/supervision.eta +10 -2
  53. package/dist/tex/templates/talks.eta +5 -1
  54. package/dist/tex/templates/teaching.eta +6 -4
  55. package/dist/tex/vitakit-cv.cls +21 -6
  56. package/dist/tex.d.mts +53 -5
  57. package/dist/tex.mjs +20 -113
  58. package/package.json +10 -7
  59. package/src/bin/vitakit.mjs +3 -153
  60. package/dist/shared/vitakit.BRhjqpAt.mjs +0 -250
  61. package/dist/shared/vitakit.BxcnlFGP.mjs +0 -325
  62. package/src/bin/vitakit-build-cv.mjs +0 -50
  63. package/src/bin/vitakit-check.mjs +0 -57
@@ -11,4 +11,4 @@ function richHtml(value) {
11
11
  return void 0;
12
12
  }
13
13
 
14
- export { richHtml as a, richSource as r };
14
+ export { richSource as a, richHtml as r };
@@ -0,0 +1,20 @@
1
+ function fieldValue(entry, field) {
2
+ if (field === "year" && !Object.hasOwn(entry, "year")) {
3
+ return entry.date?.from?.year;
4
+ }
5
+ const value = entry[field];
6
+ if (typeof value === "object" && value !== null && "text" in value) return value.text;
7
+ if (typeof value === "object" && value !== null && "label" in value) return value.label;
8
+ return value;
9
+ }
10
+ function matchesWhere(entry, where) {
11
+ return Object.entries(where).every(([field, expected]) => {
12
+ const wanted = (Array.isArray(expected) ? expected : [expected]).map(String);
13
+ return wanted.includes(String(fieldValue(entry, field) ?? ""));
14
+ });
15
+ }
16
+ function filterWhere(entries, where) {
17
+ return where && Object.keys(where).length ? entries.filter((entry) => matchesWhere(entry, where)) : [...entries];
18
+ }
19
+
20
+ export { filterWhere as a, fieldValue as f, matchesWhere as m };
@@ -0,0 +1,19 @@
1
+ const PATH_KEY = "__vitakitPath";
2
+ const isSitePath = (value) => value.startsWith("/") && !value.startsWith("//");
3
+ const sitePathMarker = (path) => ({ [PATH_KEY]: path });
4
+ function isSitePathMarker(value) {
5
+ return typeof value === "object" && value !== null && PATH_KEY in value;
6
+ }
7
+ function resolveSitePaths(value, resolve) {
8
+ if (isSitePathMarker(value)) return resolve(value[PATH_KEY]);
9
+ if (Array.isArray(value)) return value.map((item) => resolveSitePaths(item, resolve));
10
+ if (typeof value === "object" && value !== null) {
11
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolveSitePaths(item, resolve)]));
12
+ }
13
+ return value;
14
+ }
15
+ function absoluteSitePath(path, siteUrl) {
16
+ return siteUrl ? `${siteUrl.replace(/\/?$/, "/")}${path.slice(1)}` : path;
17
+ }
18
+
19
+ export { PATH_KEY as P, absoluteSitePath as a, isSitePath as i, resolveSitePaths as r, sitePathMarker as s };
@@ -0,0 +1,34 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ function sourcePath(asset) {
6
+ return asset === "styles" ? asset : join("src", asset);
7
+ }
8
+ let root;
9
+ function packageRoot() {
10
+ if (root) return root;
11
+ let dir = dirname(fileURLToPath(import.meta.url));
12
+ for (let depth = 0; depth < 6; depth += 1) {
13
+ if (existsSync(join(dir, "package.json"))) return root = dir;
14
+ const parent = dirname(dir);
15
+ if (parent === dir) break;
16
+ dir = parent;
17
+ }
18
+ throw new Error("vitakit: cannot locate the package root; the installation is incomplete");
19
+ }
20
+ const built = /[/\\]dist[/\\]/.test(import.meta.url);
21
+ function resolveAsset(asset) {
22
+ const base = packageRoot();
23
+ const order = built ? ["dist", void 0] : [void 0, "dist"];
24
+ const candidates = order.map((prefix) => prefix ? join(base, prefix, asset) : join(base, sourcePath(asset)));
25
+ for (const candidate of candidates) {
26
+ if (existsSync(candidate)) return candidate;
27
+ }
28
+ throw new Error(
29
+ `vitakit: missing bundled asset "${asset}"; looked in
30
+ ` + candidates.map((candidate) => ` ${candidate}`).join("\n") + "\nRun `pnpm build` in the package, or reinstall it."
31
+ );
32
+ }
33
+
34
+ export { packageRoot as p, resolveAsset as r };
@@ -0,0 +1,58 @@
1
+ import { pathToFileURL } from 'node:url';
2
+
3
+ class ContentError extends Error {
4
+ constructor(where, message) {
5
+ super(`${where}: ${message}`);
6
+ this.where = where;
7
+ this.name = "ContentError";
8
+ }
9
+ }
10
+ function fail(where, message) {
11
+ throw new ContentError(where, message);
12
+ }
13
+ function messageOf(error) {
14
+ if (error instanceof ContentError) return error.message.slice(error.where.length + 2);
15
+ return error instanceof Error ? error.message : String(error);
16
+ }
17
+ function whereOf(error, fallback) {
18
+ return error instanceof ContentError ? error.where : fallback;
19
+ }
20
+ async function importProjectModule(path, mtimeMs, where) {
21
+ try {
22
+ return await import(`${pathToFileURL(path).href}?mtime=${mtimeMs}`);
23
+ } catch (error) {
24
+ fail(where, importFailure(error));
25
+ }
26
+ }
27
+ function importFailure(error, node = process.versions.node) {
28
+ if (error?.code === "ERR_UNKNOWN_FILE_EXTENSION") {
29
+ return `cannot load TypeScript on Node ${node}; vitakit needs Node 22.18 or newer`;
30
+ }
31
+ return messageOf(error);
32
+ }
33
+ function formatZodIssues(error) {
34
+ return error.issues.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
35
+ }
36
+ class Issues {
37
+ constructor(strict) {
38
+ this.strict = strict;
39
+ }
40
+ list = [];
41
+ /** Record a problem, or throw it when strict. */
42
+ record(where, message) {
43
+ if (this.strict) fail(where, message);
44
+ this.list.push({ where, message });
45
+ }
46
+ /** Run `run`; when lenient, record any failure and use `fallback` instead. */
47
+ async attempt(where, run, fallback) {
48
+ try {
49
+ return await run();
50
+ } catch (error) {
51
+ if (this.strict) throw error;
52
+ this.list.push({ where: whereOf(error, where), message: messageOf(error) });
53
+ return fallback;
54
+ }
55
+ }
56
+ }
57
+
58
+ export { Issues as I, formatZodIssues as a, fail as f, importProjectModule as i, messageOf as m, whereOf as w };
@@ -0,0 +1,57 @@
1
+ const REPLACEMENTS = {
2
+ "\\": "\\textbackslash{}",
3
+ "&": "\\&",
4
+ "%": "\\%",
5
+ $: "\\$",
6
+ "#": "\\#",
7
+ _: "\\_",
8
+ "{": "\\{",
9
+ "}": "\\}",
10
+ "~": "\\textasciitilde{}",
11
+ "^": "\\textasciicircum{}"
12
+ };
13
+ const TYPOGRAPHY = [
14
+ // Directional quotes: "..." -> ``...''
15
+ [/"([^"]*)"/g, "``$1''"],
16
+ // An en dash written literally
17
+ [/\u2013/g, "--"],
18
+ [/\u2014/g, "---"],
19
+ [/\u2026/g, "\\ldots{}"],
20
+ [/\u00a0/g, "~"]
21
+ ];
22
+ function escapeTex(value, { typography = true, source } = {}) {
23
+ if (source !== void 0 && /(?<!\\)\$/.test(value)) {
24
+ throw new Error(`unbalanced math delimiters in: ${source}`);
25
+ }
26
+ let result = value.replace(/[\\&%$#_{}~^]/g, (character) => REPLACEMENTS[character]);
27
+ if (typography) {
28
+ for (const [pattern, replacement] of TYPOGRAPHY) result = result.replace(pattern, replacement);
29
+ }
30
+ return result;
31
+ }
32
+ function escapeProse(prose, source) {
33
+ if (/(?<!\\)\$/.test(prose)) throw new Error(`unbalanced math delimiters in: ${source}`);
34
+ return escapeTex(prose.replace(/\\([!-/:-@[-`{-~])/g, "$1"));
35
+ }
36
+ function escapeRichTex(value) {
37
+ const parts = [];
38
+ const pattern = /(?<!\\)\$\$[\s\S]*?\$\$|(?<!\\)\$(?:\\.|[^$\\])*\$/g;
39
+ let index = 0;
40
+ for (const match of value.matchAll(pattern)) {
41
+ const start = match.index;
42
+ parts.push(escapeProse(value.slice(index, start), value));
43
+ parts.push(match[0]);
44
+ index = start + match[0].length;
45
+ }
46
+ parts.push(escapeProse(value.slice(index), value));
47
+ return parts.join("");
48
+ }
49
+ const URL_SPECIAL = /%[0-9A-Fa-f]{2}|[%\\{}^ #&]/g;
50
+ function escapeUrl(value) {
51
+ return value.replace(URL_SPECIAL, (match) => {
52
+ if (match.length === 3 || match === "#" || match === "&") return `\\${match}`;
53
+ return `\\%${match.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`;
54
+ });
55
+ }
56
+
57
+ export { escapeTex as a, escapeUrl as b, escapeRichTex as e };
@@ -0,0 +1,142 @@
1
+ import { readFile, readdir } from 'node:fs/promises';
2
+ import { join, relative, sep } from 'node:path';
3
+
4
+ const posix = (path) => path.split(sep).join("/");
5
+ async function vueFiles(dir) {
6
+ const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
7
+ const files = [];
8
+ for (const entry of entries) {
9
+ const path = join(dir, entry.name);
10
+ if (entry.isDirectory()) files.push(...await vueFiles(path));
11
+ else if (entry.name.endsWith(".vue")) files.push(path);
12
+ }
13
+ return files.sort();
14
+ }
15
+ const pascal = (name) => name.split(/[-_.\s]+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join("");
16
+ function componentName(pathBelowComponents) {
17
+ const parts = posix(pathBelowComponents).replace(/\.vue$/, "").split("/").map(pascal);
18
+ let name = "";
19
+ for (const part of parts) name = part.startsWith(name) ? part : name + part;
20
+ return name;
21
+ }
22
+ function pageRoute(pathBelowPages) {
23
+ const path = posix(pathBelowPages).replace(/\.vue$/, "").replace(/(^|\/)index$/, "");
24
+ return path ? `/${path}/` : "/";
25
+ }
26
+ function literal(expression) {
27
+ return /^\s*(['"`])([^'"`$]*)\1\s*$/.exec(expression)?.[2];
28
+ }
29
+ const ELEMENT = 1;
30
+ const ATTRIBUTE = 6;
31
+ function attribute(node, name) {
32
+ for (const prop of node.props ?? []) {
33
+ if (prop.type === ATTRIBUTE && prop.name === name) return { static: prop.value?.content ?? "" };
34
+ if (prop.type !== ATTRIBUTE && prop.name === "bind" && prop.arg?.content === name) {
35
+ return { expression: prop.exp?.content ?? "" };
36
+ }
37
+ }
38
+ return void 0;
39
+ }
40
+ function lineOf(text, offset) {
41
+ let line = 1;
42
+ for (let index = 0; index < offset; index++) if (text.charCodeAt(index) === 10) line++;
43
+ return line;
44
+ }
45
+ async function findWebPlacements(rootDir) {
46
+ const { parse } = await import('@vue/compiler-sfc');
47
+ const appDir = join(rootDir, "app");
48
+ const componentsDir = join(appDir, "components");
49
+ const pagesDir = join(appDir, "pages");
50
+ const components = /* @__PURE__ */ new Map();
51
+ for (const file of await vueFiles(componentsDir)) {
52
+ components.set(componentName(relative(componentsDir, file)), posix(relative(rootDir, file)));
53
+ }
54
+ const placements = [];
55
+ const dynamic = [];
56
+ for (const path of await vueFiles(appDir)) {
57
+ const file = posix(relative(rootDir, path));
58
+ const inPages = !relative(pagesDir, path).startsWith("..");
59
+ const inComponents = !relative(componentsDir, path).startsWith("..");
60
+ const route = inPages ? pageRoute(relative(pagesDir, path)) : void 0;
61
+ const source = await readFile(path, "utf8");
62
+ const { descriptor } = parse(source, { filename: path });
63
+ const visit = (node) => {
64
+ if (node.type === ELEMENT && node.tag) {
65
+ const tag = node.tag.includes("-") ? pascal(node.tag) : node.tag;
66
+ if (tag === "VitaSection" || components.has(tag)) {
67
+ const id = attribute(node, "id");
68
+ const section = id?.static ?? (id?.expression !== void 0 ? literal(id.expression) : void 0);
69
+ if (section) {
70
+ const title = attribute(node, "title");
71
+ const where = attribute(node, "where");
72
+ const limit = attribute(node, "limit");
73
+ placements.push({
74
+ section,
75
+ file,
76
+ route,
77
+ line: node.loc.start.line,
78
+ component: tag,
79
+ ...title?.static !== void 0 ? { title: title.static } : {},
80
+ ...where?.expression !== void 0 ? { where: where.expression } : {},
81
+ ...limit ? { limit: limit.static ?? limit.expression } : {}
82
+ });
83
+ } else if (id?.expression !== void 0 && !inComponents) {
84
+ dynamic.push({ file, line: node.loc.start.line, usage: `<${node.tag} :id="${id.expression}">` });
85
+ }
86
+ }
87
+ }
88
+ for (const child of node.children ?? []) visit(child);
89
+ };
90
+ if (descriptor.template?.ast) visit(descriptor.template.ast);
91
+ for (const block of [descriptor.script, descriptor.scriptSetup]) {
92
+ if (!block) continue;
93
+ const start = block.loc.start.line - 1;
94
+ for (const match of block.content.matchAll(/\buseSection\(\s*([^)]*?)\s*\)/g)) {
95
+ const line = start + lineOf(block.content, match.index);
96
+ const section = literal(match[1]);
97
+ if (section) placements.push({ section, file, route, line, component: "code" });
98
+ else if (!inComponents) dynamic.push({ file, line, usage: `useSection(${match[1]})` });
99
+ }
100
+ }
101
+ }
102
+ return { placements, dynamic, components };
103
+ }
104
+ function stripComment(line) {
105
+ const match = /(^|[^\\])%/.exec(line);
106
+ return match ? line.slice(0, match.index + match[1].length) : line;
107
+ }
108
+ function findPdfPlacements(document, file, source) {
109
+ const placements = [];
110
+ const lines = source.split("\n");
111
+ let inBody = false;
112
+ for (const [index, raw] of lines.entries()) {
113
+ const line = stripComment(raw);
114
+ if (!inBody) {
115
+ if (/\\begin\{document\}/.test(line)) inBody = true;
116
+ else continue;
117
+ }
118
+ for (const match of line.matchAll(/\\vitasection(?:\[([^\]]*)\])?\{([^}]*)\}/g)) {
119
+ placements.push({
120
+ section: match[2].trim(),
121
+ document,
122
+ file,
123
+ line: index + 1,
124
+ macro: "vitasection",
125
+ ...match[1] !== void 0 ? { heading: match[1] } : {}
126
+ });
127
+ }
128
+ for (const match of line.matchAll(/\\vitainput\{([^}]*)\}/g)) {
129
+ placements.push({ section: match[1].trim(), document, file, line: index + 1, macro: "vitainput" });
130
+ }
131
+ }
132
+ return placements;
133
+ }
134
+ function documentDirective(source) {
135
+ for (const [index, raw] of source.split("\n").entries()) {
136
+ const match = /\\vitadocument\{([^}]*)\}/.exec(stripComment(raw));
137
+ if (match) return { name: match[1].trim(), line: index + 1 };
138
+ }
139
+ return void 0;
140
+ }
141
+
142
+ export { findWebPlacements as a, documentDirective as d, findPdfPlacements as f, pageRoute as p };
@@ -0,0 +1,6 @@
1
+ const LEVEL = { public: 0, cv: 1, private: 2 };
2
+ function atMost(value, limit) {
3
+ return LEVEL[value ?? "public"] <= LEVEL[limit];
4
+ }
5
+
6
+ export { atMost as a };
@@ -1,6 +1,8 @@
1
1
  <% for (const item of it.entries) { %>
2
- \vitaentry{<%~ it.date(item.date) %>}{<%~ it.rich(it.titleOf(item)) %>}{<%~ it.secondaryOf(item) %>}
3
- <% if (item.note) { %>
4
- \vitanote{<%~ it.rich(item.note) %>}
5
- <% } %>
2
+ <%~ it.vita.entry({
3
+ date: it.date(item.date),
4
+ title: it.generic.title(item),
5
+ context: it.generic.context(item)
6
+ }) %>
7
+ <%~ it.vita.note(item.note) %>
6
8
  <% } %>
@@ -1,9 +1,14 @@
1
1
  <% for (const item of it.entries) { %>
2
- \vitaentry{<%~ it.date(item.date) %>}{<%~ it.rich(item.degree) %>}{<%~ it.inst(item.institution) %>}
2
+ <%~ it.vita.entry({
3
+ date: it.date(item.date),
4
+ title: item.degree,
5
+ context: it.linked(item.institution)
6
+ }) %>
3
7
  <% if (item.thesis) { %>
4
- \vitanote{<%~ it.tex(item.thesis.type ?? 'Thesis') %>: \emph{<%~ it.rich(item.thesis.title) %>}<% if (item.thesis.supervisor) { %>, supervised by <%~ it.linkOrText(item.thesis.supervisor) %><% } %>}
5
- <% } %>
6
- <% if (item.note) { %>
7
- \vitanote{<%~ it.rich(item.note) %>}
8
+ <%~ it.vita.note(it.join([
9
+ it.optional`${item.thesis.type ?? 'Thesis'}: \emph{${item.thesis.title}}`,
10
+ it.optional`, supervised by ${it.linked(item.thesis.supervisor)}`
11
+ ], '')) %>
8
12
  <% } %>
13
+ <%~ it.vita.note(item.note) %>
9
14
  <% } %>
@@ -1,9 +1,16 @@
1
1
  <% for (const item of it.entries) { %>
2
- \vitaentry{<%~ it.date(item.date) %>}{<%~ it.rich(item.position) %>}{<%~ it.inst(item.institution) %>}
2
+ <%~ it.vita.entry({
3
+ date: it.date(item.date),
4
+ title: item.position,
5
+ context: it.linked(item.institution)
6
+ }) %>
3
7
  <% if (item.project) { %>
4
- \vitanote{Project <% if (item.project.url) { %>\href{<%~ it.url(item.project.url) %>}{\emph{<%~ it.rich(item.project.title) %>}}<% } else { %>\emph{<%~ it.rich(item.project.title) %>}<% } %><% if (item.project.funder) { %> (<%~ it.tex(item.project.funder) %><% if (item.project.grant) { %>~<%~ it.tex(item.project.grant) %><% } %>)<% } %>}
5
- <% } %>
6
- <% if (item.note) { %>
7
- \vitanote{<%~ it.rich(item.note) %>}
8
+ <%~ it.vita.note(it.join([
9
+ it.optional`Project \emph{${it.linked(item.project.title, item.project.url)}}`,
10
+ item.project.funder
11
+ ? it.optional` (${it.join([item.project.funder, item.project.grant], it.optional`~`)})`
12
+ : ''
13
+ ], '')) %>
8
14
  <% } %>
15
+ <%~ it.vita.note(item.note) %>
9
16
  <% } %>
@@ -1,7 +1,9 @@
1
+ <% if (it.groups.length) { %>
1
2
  \begin{refsection}
2
3
  \nocite{*}
3
4
  <% for (const group of it.groups) { %>
4
- <%~ it.bibgroup(group) %>
5
- \printbibliography[check=vita-<%~ group.id %>, title={<%~ it.tex(group.label) %>}, heading=subbibliography]
5
+ <%~ it.bib.group(group) %>
6
+ \printbibliography[check=vita-<%~ group.id %>, title={<%~ it.text(group.label) %>}, heading=subbibliography]
6
7
  <% } %>
7
8
  \end{refsection}
9
+ <% } %>
@@ -1,4 +1,12 @@
1
1
  <% for (const item of it.entries) { %>
2
- \vitaentry{<%~ it.date(item.date) %><% if (item.ongoing) { %>--<% } %>}{<%~ it.tex(item.student) %>}{<%~ [item.degree && it.tex(item.degree), it.inst(item.institution)].filter(Boolean).join(', ') %>}
3
- \vitanote{\emph{<%~ it.rich(item.title) %>}<% if (item.coSupervisor) { %>, with <%~ it.tex(item.coSupervisor) %><% } %><% if (item.ongoing) { %> (in progress)<% } %>}
2
+ <%~ it.vita.entry({
3
+ date: it.join([it.date(item.date), item.ongoing ? '--' : ''], ''),
4
+ title: item.student,
5
+ context: it.join([item.degree, it.linked(item.institution)])
6
+ }) %>
7
+ <%~ it.vita.note(it.join([
8
+ it.optional`\emph{${item.title}}`,
9
+ it.optional`, with ${item.coSupervisor}`,
10
+ item.ongoing ? ' (in progress)' : ''
11
+ ], '')) %>
4
12
  <% } %>
@@ -1,3 +1,7 @@
1
1
  <% for (const item of it.entries) { %>
2
- \vitaentry{<%~ it.date(item.date) %>}{<%~ it.rich(item.title) %>}{<%~ [item.event && it.tex(item.event), item.location && it.tex(item.location)].filter(Boolean).join(', ') %>}
2
+ <%~ it.vita.entry({
3
+ date: it.date(item.date),
4
+ title: item.title,
5
+ context: it.join([item.event, item.location])
6
+ }) %>
3
7
  <% } %>
@@ -1,6 +1,8 @@
1
1
  <% for (const item of it.entries) { %>
2
- \vitaentry{<%~ it.term(item) %>}{<%~ it.rich(item.title) %>}{<%~ [item.role, it.inst(item.institution)].filter(Boolean).join(', ') %>}
3
- <% if (item.note) { %>
4
- \vitanote{<%~ it.rich(item.note) %>}
5
- <% } %>
2
+ <%~ it.vita.entry({
3
+ date: it.text(item.term) || it.date(item.date),
4
+ title: item.title,
5
+ context: it.join([item.role, it.linked(item.institution)])
6
+ }) %>
7
+ <%~ it.vita.note(item.note) %>
6
8
  <% } %>
@@ -13,6 +13,15 @@
13
13
  \ProcessOptions\relax
14
14
  \LoadClass[11pt,a4paper]{article}
15
15
 
16
+ %% pdfLaTeX's default font encoding (OT1) has no „, ‚, ł, ő and many other
17
+ %% characters of names, places and titles; T1 has them. LuaLaTeX and XeLaTeX
18
+ %% use Unicode fonts instead.
19
+ \RequirePackage{iftex}
20
+ \ifPDFTeX
21
+ \RequirePackage[T1]{fontenc}
22
+ \RequirePackage{lmodern}
23
+ \fi
24
+
16
25
  \RequirePackage[margin=2.5cm]{geometry}
17
26
  \RequirePackage{enumitem}
18
27
  \RequirePackage{xcolor}
@@ -105,12 +114,18 @@
105
114
  %% that it is safe after any preceding content, including none.
106
115
  \newcommand{\vitanote}[1]{\par{\small\color{vitamuted}#1}}
107
116
 
108
- %% \vitasection{id} -- heading plus the generated file for this document.
109
- \newcommand{\vitasection}[1]{%
110
- \section{\vita@titlefor{#1}}%
111
- \begin{vitaentries}%
112
- \input{generated/\vita@document/#1.tex}%
113
- \end{vitaentries}}
117
+ %% Generated profile.tex marks sections whose selection is empty.
118
+ \newcommand{\vitaemptysection}[1]{\expandafter\def\csname vita@empty@#1\endcsname{}}
119
+
120
+ %% \vitasection[Heading]{id} -- an optional heading for this invocation only.
121
+ %% Otherwise use the document/preamble label, falling back to the section id.
122
+ \NewDocumentCommand{\vitasection}{o m}{%
123
+ \ifcsname vita@empty@#2\endcsname\else
124
+ \IfNoValueTF{#1}{\section{\vita@titlefor{#2}}}{\section{#1}}%
125
+ \begin{vitaentries}%
126
+ \input{generated/\vita@document/#2.tex}%
127
+ \end{vitaentries}%
128
+ \fi}
114
129
 
115
130
  %% \vitainput{id} -- the generated file without heading or list wrapper,
116
131
  %% for sections that bring their own structure (publications).
package/dist/tex.d.mts CHANGED
@@ -7,6 +7,28 @@ interface BuildOptions {
7
7
  only?: string[];
8
8
  /** Emit .tex but skip latexmk. */
9
9
  noPdf?: boolean;
10
+ /**
11
+ * What happens to latexmk's output: shown as it runs (`inherit`, the
12
+ * default), or kept back and only its end shown on failure (`capture`).
13
+ */
14
+ latexOutput?: 'inherit' | 'capture';
15
+ /** Told about each finished step, for concise progress output. */
16
+ onProgress?: (event: BuildEvent) => void;
17
+ }
18
+ type BuildEvent = {
19
+ document: string;
20
+ step: 'tex';
21
+ dir: string;
22
+ files: number;
23
+ } | {
24
+ document: string;
25
+ step: 'pdf';
26
+ file: string;
27
+ };
28
+ /** A tool of the TeX distribution is not installed. */
29
+ declare class MissingToolError extends Error {
30
+ readonly tool: string;
31
+ constructor(tool: string);
10
32
  }
11
33
  declare function buildCv(options: BuildOptions): Promise<string[]>;
12
34
  /** Read a generated file, for tests and diagnostics. */
@@ -19,7 +41,7 @@ declare const documentSchema: z.ZodObject<{
19
41
  private: "private";
20
42
  }>>;
21
43
  sections: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
22
- where: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>>>;
44
+ where: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>, z.ZodArray<z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean]>>]>>>;
23
45
  limit: z.ZodOptional<z.ZodNumber>;
24
46
  since: z.ZodOptional<z.ZodNumber>;
25
47
  until: z.ZodOptional<z.ZodNumber>;
@@ -36,13 +58,31 @@ interface DocumentSource {
36
58
  texFile: string;
37
59
  options: DocumentOptions;
38
60
  }
39
- /** Read every `cv-tex/*.tex` and its options. */
61
+ /** Read every `cv-tex/*.tex` and its options. Strict: the first problem throws. */
40
62
  declare function discoverDocuments(cvDir: string): Promise<DocumentSource[]>;
41
63
  /** Select the entries of one section for one document. */
42
64
  declare function selectEntries(entries: Entry[], options: DocumentOptions, sectionId: string): Entry[];
43
65
 
44
66
  /** `tex/` in the installed package: templates and the document class. */
45
67
  declare const bundledTexDir: string;
68
+ /** Where a template came from, in the order they are tried. */
69
+ interface ResolvedTemplate {
70
+ path: string;
71
+ source: string;
72
+ /** `local` is in `cv-tex/templates/`, `bundled` is the package's own. */
73
+ origin: 'local' | 'bundled';
74
+ /** Whether this is `_generic.eta` rather than a template named for the section. */
75
+ generic: boolean;
76
+ }
77
+ /**
78
+ * The template that renders section `id`. A file in the repo always wins over
79
+ * the package default; `_generic.eta` is the fallback for sections without a
80
+ * dedicated template. `exclude` skips one path, so that replacing a local
81
+ * template can start from whatever would be used without it.
82
+ */
83
+ declare function resolveTemplate(id: string, userDir: string, { exclude }?: {
84
+ exclude?: string;
85
+ }): Promise<ResolvedTemplate>;
46
86
  interface EmitOptions {
47
87
  sections: Record<string, Entry[]>;
48
88
  publications: PublicationGroup[];
@@ -56,6 +96,13 @@ interface EmitOptions {
56
96
  /** Path of the .bib file, relative to the generated directory. */
57
97
  bibPath?: string;
58
98
  }
99
+ /**
100
+ * Render one section with its effective template, without writing anything.
101
+ * `entries` are all loaded entries; the document's selection is applied here.
102
+ */
103
+ declare function renderSection(id: string, entries: Entry[], options: DocumentOptions, templateDir: string): Promise<string>;
104
+ /** Render the publications of one document, without writing anything. */
105
+ declare function renderPublications(publications: PublicationGroup[], options: DocumentOptions, templateDir: string, siteUrl?: string): Promise<string>;
59
106
  /** Write one `.tex` file per section, plus `publications.tex`. */
60
107
  declare function emitDocument(emit: EmitOptions): Promise<string[]>;
61
108
 
@@ -84,10 +131,11 @@ declare function escapeTex(value: string, { typography, source }?: EscapeOptions
84
131
  * Escape prose while preserving `$...$` and `$$...$$` spans verbatim.
85
132
  *
86
133
  * Used for `richText` fields, where a mathematician writes `Random $k$-ary
87
- * trees` and expects the formula to reach LaTeX untouched.
134
+ * trees` and expects the formula to reach LaTeX untouched. The prose is
135
+ * Markdown, so `\$` is a dollar sign and does not start a formula.
88
136
  */
89
137
  declare function escapeRichTex(value: string): string;
90
138
  declare function escapeUrl(value: string): string;
91
139
 
92
- export { buildCv, bundledTexDir, discoverDocuments, documentSchema, emitDocument, escapeRichTex, escapeTex, escapeUrl, readGenerated, selectEntries };
93
- export type { BuildOptions, DocumentOptions, DocumentSource, EmitOptions, EscapeOptions };
140
+ export { MissingToolError, buildCv, bundledTexDir, discoverDocuments, documentSchema, emitDocument, escapeRichTex, escapeTex, escapeUrl, readGenerated, renderPublications, renderSection, resolveTemplate, selectEntries };
141
+ export type { BuildEvent, BuildOptions, DocumentOptions, DocumentSource, EmitOptions, EscapeOptions, ResolvedTemplate };