@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.
- package/README.md +51 -131
- package/dist/check.mjs +12 -5
- package/dist/chunks/authoring.mjs +367 -0
- package/dist/chunks/build.mjs +176 -0
- package/dist/chunks/documents.mjs +143 -0
- package/dist/chunks/draft.mjs +222 -0
- package/dist/chunks/eject.mjs +59 -0
- package/dist/chunks/emit.mjs +200 -0
- package/dist/chunks/eta.mjs +16 -0
- package/dist/chunks/fields.mjs +79 -0
- package/dist/chunks/inventory.mjs +199 -0
- package/dist/chunks/plan.mjs +144 -0
- package/dist/chunks/prompt.mjs +33 -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 +124 -0
- package/dist/chunks/vue.mjs +116 -0
- package/dist/cli.d.mts +33 -40
- package/dist/cli.mjs +7 -224
- package/dist/index.d.mts +372 -42
- package/dist/index.mjs +10 -4
- package/dist/nuxt.mjs +35 -5
- package/dist/runtime/components/VitaSection.vue +94 -38
- package/dist/schemas/education.ts +4 -4
- package/dist/schemas/employment.ts +3 -3
- package/dist/schemas/publications.ts +2 -2
- 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.BZZIsWlY.mjs +179 -0
- package/dist/shared/vitakit.BerUaxtF.mjs +121 -0
- package/dist/shared/{vitakit.D8WRg_Xx.d.mts → vitakit.BfpxDtz1.d.mts} +9 -8
- package/dist/shared/{vitakit.CypCF-4Q.mjs → vitakit.By5VWQ1n.mjs} +12 -78
- package/dist/shared/vitakit.CfB3hMHl.mjs +60 -0
- package/dist/shared/vitakit.CoWX2Z4R.mjs +1554 -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.DQ6PmGoL.mjs +34 -0
- package/dist/shared/vitakit.DbSeENVS.mjs +58 -0
- package/dist/shared/vitakit.DcQOdM4I.mjs +57 -0
- package/dist/shared/vitakit.Do5HC_MD.mjs +142 -0
- package/dist/shared/vitakit.TSMTC5Jl.mjs +6 -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 +4 -2
- 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 +21 -6
- package/dist/tex.d.mts +53 -5
- package/dist/tex.mjs +20 -113
- 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
|
@@ -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 };
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<% for (const item of it.entries) { %>
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
|
5
|
-
\printbibliography[check=vita-<%~ group.id %>, title={<%~ it.
|
|
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
|
-
|
|
3
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
<% } %>
|
package/dist/tex/vitakit-cv.cls
CHANGED
|
@@ -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
|
-
%%
|
|
109
|
-
\newcommand{\
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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 };
|