@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
package/dist/tex.mjs
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
import
|
|
6
|
-
import
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
|
|
1
|
+
export { MissingToolError, buildCv, readGenerated } from './chunks/build.mjs';
|
|
2
|
+
export { bundledTexDir, emitDocument, renderPublications, renderSection, resolveTemplate } from './chunks/emit.mjs';
|
|
3
|
+
export { discoverDocuments, documentSchema, selectEntries } from './chunks/documents.mjs';
|
|
4
|
+
export { e as escapeRichTex, a as escapeTex, b as escapeUrl } from './shared/vitakit.DcQOdM4I.mjs';
|
|
5
|
+
import 'node:child_process';
|
|
6
|
+
import 'node:fs/promises';
|
|
7
|
+
import 'node:fs';
|
|
8
|
+
import 'node:process';
|
|
9
|
+
import 'node:path';
|
|
10
|
+
import './shared/vitakit.By5VWQ1n.mjs';
|
|
10
11
|
import 'gray-matter';
|
|
11
12
|
import 'yaml';
|
|
12
13
|
import 'unified';
|
|
@@ -18,112 +19,18 @@ import 'rehype-katex';
|
|
|
18
19
|
import 'rehype-raw';
|
|
19
20
|
import 'rehype-stringify';
|
|
20
21
|
import 'unist-util-visit';
|
|
22
|
+
import './shared/vitakit.D10HyDhF.mjs';
|
|
23
|
+
import './shared/vitakit.DQ6PmGoL.mjs';
|
|
24
|
+
import 'node:url';
|
|
21
25
|
import '@behackl/citation-js-extras';
|
|
22
26
|
import 'rehype-parse';
|
|
23
27
|
import 'rehype-sanitize';
|
|
28
|
+
import './shared/vitakit.DbSeENVS.mjs';
|
|
24
29
|
import 'zod';
|
|
25
|
-
import '
|
|
30
|
+
import './chunks/sections.mjs';
|
|
31
|
+
import './chunks/schemas.mjs';
|
|
32
|
+
import './shared/vitakit.BerUaxtF.mjs';
|
|
33
|
+
import './shared/vitakit.TSMTC5Jl.mjs';
|
|
26
34
|
import 'eta';
|
|
27
|
-
import './shared/
|
|
28
|
-
import './shared/vitakit.
|
|
29
|
-
|
|
30
|
-
const CLASS_FILE = "vitakit-cv.cls";
|
|
31
|
-
function run(command, args, cwd, env = {}) {
|
|
32
|
-
return new Promise((resolve, reject) => {
|
|
33
|
-
const child = spawn(command, args, { cwd, stdio: "inherit", env: { ...process.env, ...env } });
|
|
34
|
-
child.on("error", (error) => reject(
|
|
35
|
-
error.code === "ENOENT" ? new Error(`${command} not found. Install a TeX distribution, or use --no-pdf to only generate .tex files.`) : error
|
|
36
|
-
));
|
|
37
|
-
child.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`${command} exited with code ${code}`)));
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
function profilePreamble(content) {
|
|
41
|
-
const { profile } = content;
|
|
42
|
-
const href = (url) => {
|
|
43
|
-
if (isSitePath(url)) fail("content/profile.yml", `links: ${url} ${SITE_PATH_HELP}`);
|
|
44
|
-
return escapeUrl(url);
|
|
45
|
-
};
|
|
46
|
-
const affiliation = profile.affiliation?.name ? escapeTex(profile.affiliation.name) : "";
|
|
47
|
-
const tagline = [profile.title && escapeTex(profile.title), affiliation].filter(Boolean).join(", ");
|
|
48
|
-
const contact = [
|
|
49
|
-
profile.email && `\\href{mailto:${escapeUrl(profile.email)}}{${escapeTex(profile.email)}}`,
|
|
50
|
-
...profile.links.map((link) => `\\href{${href(link.url)}}{${escapeTex(link.label)}}`)
|
|
51
|
-
].filter(Boolean).join(" \\quad ");
|
|
52
|
-
return [
|
|
53
|
-
"% Generated by vitakit. Do not edit; edit content/profile.yml instead.",
|
|
54
|
-
`\\vitaname{${escapeTex(profile.name)}}`,
|
|
55
|
-
`\\vitatagline{${tagline}}`,
|
|
56
|
-
`\\vitacontact{${contact}}`,
|
|
57
|
-
...profile.url ? siteUrlSourcemap(profile.url) : [],
|
|
58
|
-
""
|
|
59
|
-
].join("\n");
|
|
60
|
-
}
|
|
61
|
-
function siteUrlSourcemap(siteUrl) {
|
|
62
|
-
const base = siteUrl.replace(/\/?$/, "/");
|
|
63
|
-
if (!/^https?:\/\/[\w.:/-]+$/.test(base)) {
|
|
64
|
-
fail("content/profile.yml", `url: ${siteUrl} may only contain letters, digits and - . _ : /`);
|
|
65
|
-
}
|
|
66
|
-
return [
|
|
67
|
-
"\\ifdefined\\DeclareSourcemap",
|
|
68
|
-
` \\DeclareSourcemap{\\maps[datatype=bibtex]{\\map{\\step[fieldsource=url, match=\\regexp{\\A/}, replace={${base}}]}}}`,
|
|
69
|
-
"\\fi"
|
|
70
|
-
];
|
|
71
|
-
}
|
|
72
|
-
async function buildDocument(document, content, options, cvDir) {
|
|
73
|
-
const outDir = join(cvDir, "generated", document.name);
|
|
74
|
-
await emitDocument({
|
|
75
|
-
sections: content.sections,
|
|
76
|
-
publications: content.publications,
|
|
77
|
-
siteUrl: content.profile.url,
|
|
78
|
-
options: document.options,
|
|
79
|
-
templateDir: join(cvDir, "templates"),
|
|
80
|
-
outDir
|
|
81
|
-
});
|
|
82
|
-
await writeFile(join(outDir, "profile.tex"), profilePreamble(content));
|
|
83
|
-
const generatedClass = join(outDir, CLASS_FILE);
|
|
84
|
-
if (existsSync(join(cvDir, CLASS_FILE))) await rm(generatedClass, { force: true });
|
|
85
|
-
else await copyFile(join(bundledTexDir, CLASS_FILE), generatedClass);
|
|
86
|
-
if (options.noPdf) return;
|
|
87
|
-
const relativeOut = join("generated", document.name);
|
|
88
|
-
await run("latexmk", [
|
|
89
|
-
"-pdf",
|
|
90
|
-
"-halt-on-error",
|
|
91
|
-
"-interaction=nonstopmode",
|
|
92
|
-
`-outdir=${relativeOut}`,
|
|
93
|
-
`${document.name}.tex`
|
|
94
|
-
], cvDir, {
|
|
95
|
-
// A trailing delimiter keeps the standard search paths.
|
|
96
|
-
TEXINPUTS: `.${delimiter}${relativeOut}${delimiter}${process.env.TEXINPUTS ?? ""}`
|
|
97
|
-
});
|
|
98
|
-
await rename(join(outDir, `${document.name}.pdf`), join(cvDir, `${document.name}.pdf`));
|
|
99
|
-
}
|
|
100
|
-
async function buildCv(options) {
|
|
101
|
-
const cvDir = join(options.rootDir, "cv-tex");
|
|
102
|
-
if (!existsSync(cvDir)) {
|
|
103
|
-
throw new Error(`no cv-tex/ directory in ${options.rootDir}`);
|
|
104
|
-
}
|
|
105
|
-
const content = await loadContent(options.rootDir, { strict: true, maxVisibility: "private", target: "pdf" });
|
|
106
|
-
const documents = await discoverDocuments(cvDir);
|
|
107
|
-
const groups = (await loadPublicationsSchema(options.rootDir)).groups.map((group) => group.id);
|
|
108
|
-
for (const document of documents) {
|
|
109
|
-
const unknown = (document.options.publications.groups ?? []).filter((id) => !groups.includes(id));
|
|
110
|
-
if (unknown.length) {
|
|
111
|
-
fail(`cv-tex/${document.name}.yml`, `publications.groups: unknown group ${unknown.join(", ")}; the groups are ${groups.join(", ")} (schemas/publications.ts)`);
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
const selected = options.only?.length ? documents.filter((document) => options.only.includes(document.name)) : documents;
|
|
115
|
-
if (!selected.length) {
|
|
116
|
-
const known = documents.map((document) => document.name).join(", ");
|
|
117
|
-
throw new Error(`no document matched ${options.only?.join(", ")}; available: ${known}`);
|
|
118
|
-
}
|
|
119
|
-
await mkdir(join(cvDir, "generated"), { recursive: true });
|
|
120
|
-
for (const document of selected) {
|
|
121
|
-
await buildDocument(document, content, options, cvDir);
|
|
122
|
-
}
|
|
123
|
-
return selected.map((document) => join(cvDir, `${document.name}.pdf`));
|
|
124
|
-
}
|
|
125
|
-
async function readGenerated(rootDir, document, section) {
|
|
126
|
-
return readFile(join(rootDir, "cv-tex", "generated", document, `${section}.tex`), "utf8");
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export { buildCv, bundledTexDir, discoverDocuments, emitDocument, escapeTex, escapeUrl, readGenerated };
|
|
35
|
+
import './shared/vitakit.CfB3hMHl.mjs';
|
|
36
|
+
import './shared/vitakit.D-DdJaDZ.mjs';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anja.codes/vitakit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Structured CV data in, academic website and LaTeX CV out.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"katex",
|
|
14
14
|
"static-site"
|
|
15
15
|
],
|
|
16
|
-
"homepage": "https://
|
|
16
|
+
"homepage": "https://vitakit.anja.codes",
|
|
17
17
|
"bugs": {
|
|
18
18
|
"url": "https://github.com/anja-codes/vitakit/issues"
|
|
19
19
|
},
|
|
@@ -69,16 +69,20 @@
|
|
|
69
69
|
"test": "vitest run",
|
|
70
70
|
"test:watch": "vitest",
|
|
71
71
|
"typecheck": "tsc --noEmit",
|
|
72
|
-
"check": "pnpm typecheck && pnpm
|
|
72
|
+
"check": "pnpm typecheck && pnpm build && node scripts/check-dist.mjs && pnpm test && pnpm example:check",
|
|
73
73
|
"check:dist": "node scripts/check-dist.mjs",
|
|
74
74
|
"prepublishOnly": "pnpm check",
|
|
75
75
|
"example": "node scripts/generate-example.mjs",
|
|
76
76
|
"example:check": "node scripts/generate-example.mjs --check",
|
|
77
|
-
"check:deps": "node scripts/check-deps.mjs"
|
|
77
|
+
"check:deps": "node scripts/check-deps.mjs",
|
|
78
|
+
"docs:dev": "pnpm --filter ./docs dev",
|
|
79
|
+
"docs:build": "pnpm build && pnpm --filter ./docs build"
|
|
78
80
|
},
|
|
79
81
|
"dependencies": {
|
|
82
|
+
"@babel/parser": "^7.29.9",
|
|
80
83
|
"@behackl/citation-js-extras": "^0.4.0",
|
|
81
84
|
"@types/hast": "^3.0.5",
|
|
85
|
+
"@vue/compiler-sfc": "^3.5.43",
|
|
82
86
|
"citation-js": "^0.7.22",
|
|
83
87
|
"eta": "^4.6.0",
|
|
84
88
|
"gray-matter": "^4.0.3",
|
|
@@ -101,6 +105,7 @@
|
|
|
101
105
|
"nuxt": "^4.0.0"
|
|
102
106
|
},
|
|
103
107
|
"devDependencies": {
|
|
108
|
+
"@babel/types": "^7.29.8",
|
|
104
109
|
"@nuxt/kit": "^4.5.2",
|
|
105
110
|
"@nuxt/schema": "^4.5.2",
|
|
106
111
|
"@types/node": "^26.4.1",
|
|
@@ -114,8 +119,6 @@
|
|
|
114
119
|
"access": "public"
|
|
115
120
|
},
|
|
116
121
|
"bin": {
|
|
117
|
-
"vitakit": "src/bin/vitakit.mjs"
|
|
118
|
-
"vitakit-check": "src/bin/vitakit-check.mjs",
|
|
119
|
-
"vitakit-build-cv": "src/bin/vitakit-build-cv.mjs"
|
|
122
|
+
"vitakit": "src/bin/vitakit.mjs"
|
|
120
123
|
}
|
|
121
124
|
}
|
package/src/bin/vitakit.mjs
CHANGED
|
@@ -1,158 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* The vitakit command
|
|
4
|
-
* could have written yourself — see docs/cli.md for why there are not more.
|
|
3
|
+
* The vitakit command; see `pnpm vitakit --help` or docs/cli/.
|
|
5
4
|
*/
|
|
6
5
|
import process from 'node:process'
|
|
7
|
-
import {
|
|
8
|
-
import { addSection, eject, listEjectable } from '../../dist/cli.mjs'
|
|
6
|
+
import { main } from '../../dist/cli.mjs'
|
|
9
7
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
Usage
|
|
13
|
-
pnpm vitakit <command> [options]
|
|
14
|
-
|
|
15
|
-
Commands
|
|
16
|
-
add <name> set up a new section: content file, schema, component
|
|
17
|
-
list show everything that can be ejected
|
|
18
|
-
eject <name> copy a package default into your project
|
|
19
|
-
|
|
20
|
-
Run a command with --help for details and examples.
|
|
21
|
-
|
|
22
|
-
Related commands, from the same package:
|
|
23
|
-
pnpm build:cv render content/ into cv-tex/ and run latexmk
|
|
24
|
-
pnpm check typecheck, generate the site, and verify the output
|
|
25
|
-
`
|
|
26
|
-
|
|
27
|
-
const HELP = {
|
|
28
|
-
add: `vitakit add <name> — set up a new section
|
|
29
|
-
|
|
30
|
-
A section is a file in content/, a schema describing what it may contain, and
|
|
31
|
-
usually a component deciding how it looks. This writes whichever of those are
|
|
32
|
-
missing; it never overwrites a file you have.
|
|
33
|
-
|
|
34
|
-
pnpm vitakit add outreach
|
|
35
|
-
|
|
36
|
-
content/outreach.yml your data — edit this first
|
|
37
|
-
schemas/outreach.ts the shape of an entry
|
|
38
|
-
app/components/OutreachSection.vue how the entries look
|
|
39
|
-
|
|
40
|
-
If content/<name>.yml already exists, the schema is drafted from it: field
|
|
41
|
-
types are guessed from your data — dates, URLs, enums, and which fields are
|
|
42
|
-
optional — so you correct a draft instead of writing one.
|
|
43
|
-
|
|
44
|
-
pnpm vitakit add outreach # after writing content/outreach.yml
|
|
45
|
-
|
|
46
|
-
Options
|
|
47
|
-
--no-component do not write a component; the section then renders through
|
|
48
|
-
<VitaSection>, which is a fallback rather than a design
|
|
49
|
-
|
|
50
|
-
Notes
|
|
51
|
-
The name becomes a file name: lowercase, kebab-case (research-stays).
|
|
52
|
-
For a built-in section (education, employment, teaching, talks,
|
|
53
|
-
supervision) you do not need this: create content/<name>.yml and it works.
|
|
54
|
-
To change what a built-in section may contain, use \`vitakit eject <name>\`.
|
|
55
|
-
|
|
56
|
-
Afterwards
|
|
57
|
-
Show it on a page with <OutreachSection /> in app/pages/, or put
|
|
58
|
-
\\vitasection{outreach} in cv-tex/cv.tex to add it to the PDF.`,
|
|
59
|
-
|
|
60
|
-
list: `vitakit list — show everything that can be ejected
|
|
61
|
-
|
|
62
|
-
Prints each item, what kind of thing it is, and where a copy would land:
|
|
63
|
-
|
|
64
|
-
VitaSection component -> app/components/VitaSection.vue
|
|
65
|
-
talks.eta template -> cv-tex/templates/talks.eta
|
|
66
|
-
teaching schema -> schemas/teaching.ts
|
|
67
|
-
publications schema -> schemas/publications.ts
|
|
68
|
-
publications.csl style -> content/publications.csl
|
|
69
|
-
vitakit-cv.cls class -> cv-tex/vitakit-cv.cls
|
|
70
|
-
|
|
71
|
-
component how a section looks on the website
|
|
72
|
-
schema what a section may contain; for publications, the groups,
|
|
73
|
-
the links after each reference and the fields passed on
|
|
74
|
-
template how a section is written into the PDF
|
|
75
|
-
style how publications.bib is formatted on the website (CSL)
|
|
76
|
-
class the LaTeX document class: fonts, spacing, the entry macros`,
|
|
77
|
-
|
|
78
|
-
eject: `vitakit eject <name> — copy a package default into your project
|
|
79
|
-
|
|
80
|
-
The copy wins from then on: vitakit always prefers a file in your project over
|
|
81
|
-
its own. This is the only command that exists purely for convenience — the
|
|
82
|
-
files live inside node_modules/.pnpm/…, at a path that changes with every
|
|
83
|
-
update, so copying them by hand is impractical.
|
|
84
|
-
|
|
85
|
-
pnpm vitakit eject VitaSection # restyle how sections look
|
|
86
|
-
pnpm vitakit eject teaching # add or remove fields of a section
|
|
87
|
-
pnpm vitakit eject talks.eta # change how talks appear in the PDF
|
|
88
|
-
pnpm vitakit eject publications.csl # change the citation style of the website
|
|
89
|
-
pnpm vitakit eject vitakit-cv.cls # take over the LaTeX document class
|
|
90
|
-
|
|
91
|
-
Use \`vitakit list\` to see the names.
|
|
92
|
-
|
|
93
|
-
Options
|
|
94
|
-
--force overwrite an existing file
|
|
95
|
-
|
|
96
|
-
Trade-off
|
|
97
|
-
An ejected file is yours forever: later improvements to it in vitakit will
|
|
98
|
-
not reach you. Everything that can carry a bug — validation, rendering,
|
|
99
|
-
sorting, escaping — stays in the package, so this is usually the right
|
|
100
|
-
trade, but eject deliberately rather than by default.
|
|
101
|
-
|
|
102
|
-
Before ejecting a component, check whether CSS is enough: the classes
|
|
103
|
-
(vita-entry, vita-entry__date, …) are styled in app/assets/css/main.css.`,
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const [command, ...rest] = process.argv.slice(2)
|
|
107
|
-
const wantsHelp = rest.includes('--help') || rest.includes('-h')
|
|
108
|
-
const root = process.cwd()
|
|
109
|
-
const pascal = name => name.split('-').map(part => part[0].toUpperCase() + part.slice(1)).join('')
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
if (!command || command === '--help' || command === '-h' || command === 'help') {
|
|
113
|
-
console.log(USAGE)
|
|
114
|
-
process.exit(command ? 0 : 1)
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (!HELP[command]) {
|
|
118
|
-
throw new Error(`unknown command "${command}".\n\n${USAGE}`)
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
if (wantsHelp) {
|
|
122
|
-
console.log(HELP[command])
|
|
123
|
-
process.exit(0)
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
if (command === 'add') {
|
|
127
|
-
const name = rest.find(argument => !argument.startsWith('-'))
|
|
128
|
-
if (!name) throw new Error(`missing name.\n\n${HELP.add}`)
|
|
129
|
-
const written = await addSection(name, root, { component: !rest.includes('--no-component') })
|
|
130
|
-
for (const file of written) console.log(` ${file.path.padEnd(34)} ${file.note}`)
|
|
131
|
-
console.log(`\nShow it with <${pascal(name)}Section /> on a page,`)
|
|
132
|
-
console.log(`or add \\vitasection{${name}} to cv-tex/cv.tex for the PDF.`)
|
|
133
|
-
} else if (command === 'list') {
|
|
134
|
-
const items = await listEjectable()
|
|
135
|
-
const width = Math.max(...items.map(item => item.name.length))
|
|
136
|
-
for (const item of items) {
|
|
137
|
-
console.log(`${item.name.padEnd(width)} ${item.kind.padEnd(9)} -> ${item.target}`)
|
|
138
|
-
}
|
|
139
|
-
console.log('\nCopy one with `pnpm vitakit eject <name>`; see --help for what each kind does.')
|
|
140
|
-
} else if (command === 'eject') {
|
|
141
|
-
const name = rest.find(argument => !argument.startsWith('-'))
|
|
142
|
-
if (!name) throw new Error(`missing name. Run \`pnpm vitakit list\` to see the options.`)
|
|
143
|
-
const { item, path } = await eject(name, root, { force: rest.includes('--force') })
|
|
144
|
-
console.log(`vitakit: wrote ${relative(root, path)}`)
|
|
145
|
-
const hints = {
|
|
146
|
-
publications: ' Edit it to change the publication groups, badges or fields; the website,\n'
|
|
147
|
-
+ ' vitakit-check and the PDF all read it from now on.',
|
|
148
|
-
schema: ' Edit it to change what this section may contain; it now defines the section.',
|
|
149
|
-
style: ' Edit it, or replace it with any CSL style (e.g. from zotero.org/styles);\n'
|
|
150
|
-
+ ' it formats content/publications.bib on the website from now on.',
|
|
151
|
-
}
|
|
152
|
-
const hint = item.kind === 'schema' && item.name === 'publications' ? hints.publications : hints[item.kind]
|
|
153
|
-
console.log(hint ?? ' It takes precedence over the package copy from now on.')
|
|
154
|
-
}
|
|
155
|
-
} catch (error) {
|
|
156
|
-
console.error(`\nvitakit: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
157
|
-
process.exit(1)
|
|
158
|
-
}
|
|
8
|
+
process.exitCode = await main(process.argv.slice(2))
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
import { readdir, readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { existsSync } from 'node:fs';
|
|
3
|
-
import { basename, join } from 'node:path';
|
|
4
|
-
import { Eta } from 'eta';
|
|
5
|
-
import { m as fail, p as formatZodIssues, k as atMost, o as resolveAsset, n as isSitePath } from './vitakit.BxcnlFGP.mjs';
|
|
6
|
-
import { entrySecondaryParts, entryTitleField, formatDateRange } from './index.mjs';
|
|
7
|
-
import { r as richSource } from './vitakit.BlU-p01e.mjs';
|
|
8
|
-
import { parse } from 'yaml';
|
|
9
|
-
import { z } from 'zod';
|
|
10
|
-
|
|
11
|
-
const REPLACEMENTS = {
|
|
12
|
-
"\\": "\\textbackslash{}",
|
|
13
|
-
"&": "\\&",
|
|
14
|
-
"%": "\\%",
|
|
15
|
-
$: "\\$",
|
|
16
|
-
"#": "\\#",
|
|
17
|
-
_: "\\_",
|
|
18
|
-
"{": "\\{",
|
|
19
|
-
"}": "\\}",
|
|
20
|
-
"~": "\\textasciitilde{}",
|
|
21
|
-
"^": "\\textasciicircum{}"
|
|
22
|
-
};
|
|
23
|
-
const TYPOGRAPHY = [
|
|
24
|
-
// Directional quotes: "..." -> ``...''
|
|
25
|
-
[/"([^"]*)"/g, "``$1''"],
|
|
26
|
-
// An en dash written literally
|
|
27
|
-
[/\u2013/g, "--"],
|
|
28
|
-
[/\u2014/g, "---"],
|
|
29
|
-
[/\u2026/g, "\\ldots{}"],
|
|
30
|
-
[/\u00a0/g, "~"]
|
|
31
|
-
];
|
|
32
|
-
function escapeTex(value, { typography = true, source } = {}) {
|
|
33
|
-
if (source !== void 0 && /(?<!\\)\$/.test(value)) {
|
|
34
|
-
throw new Error(`unbalanced math delimiters in: ${source}`);
|
|
35
|
-
}
|
|
36
|
-
let result = value.replace(/[\\&%$#_{}~^]/g, (character) => REPLACEMENTS[character]);
|
|
37
|
-
if (typography) {
|
|
38
|
-
for (const [pattern, replacement] of TYPOGRAPHY) result = result.replace(pattern, replacement);
|
|
39
|
-
}
|
|
40
|
-
return result;
|
|
41
|
-
}
|
|
42
|
-
function escapeRichTex(value) {
|
|
43
|
-
const parts = [];
|
|
44
|
-
const pattern = /\$\$[\s\S]*?\$\$|\$(?:\\.|[^$\\])*\$/g;
|
|
45
|
-
let index = 0;
|
|
46
|
-
for (const match of value.matchAll(pattern)) {
|
|
47
|
-
const start = match.index;
|
|
48
|
-
parts.push(escapeTex(value.slice(index, start), { source: value }));
|
|
49
|
-
parts.push(match[0]);
|
|
50
|
-
index = start + match[0].length;
|
|
51
|
-
}
|
|
52
|
-
parts.push(escapeTex(value.slice(index), { source: value }));
|
|
53
|
-
return parts.join("");
|
|
54
|
-
}
|
|
55
|
-
const URL_SPECIAL = /%[0-9A-Fa-f]{2}|[%\\{}^ #&]/g;
|
|
56
|
-
function escapeUrl(value) {
|
|
57
|
-
return value.replace(URL_SPECIAL, (match) => {
|
|
58
|
-
if (match.length === 3 || match === "#" || match === "&") return `\\${match}`;
|
|
59
|
-
return `\\%${match.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`;
|
|
60
|
-
});
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const sectionOptions = z.strictObject({
|
|
64
|
-
/**
|
|
65
|
-
* Keep entries whose field equals the value, or one of several values:
|
|
66
|
-
*
|
|
67
|
-
* ```yaml
|
|
68
|
-
* where: { kind: invited }
|
|
69
|
-
* where: { kind: [invited, seminar] }
|
|
70
|
-
* ```
|
|
71
|
-
*/
|
|
72
|
-
where: z.record(z.string(), z.union([z.string(), z.array(z.string()).min(1)])).default({}),
|
|
73
|
-
/** Keep only the newest N entries. */
|
|
74
|
-
limit: z.number().int().positive().optional(),
|
|
75
|
-
/** Drop entries starting before this year. */
|
|
76
|
-
since: z.number().int().optional(),
|
|
77
|
-
/** Drop entries starting after this year. */
|
|
78
|
-
until: z.number().int().optional(),
|
|
79
|
-
/** Heading used by `\vitasection`; defaults to the section id, capitalised. */
|
|
80
|
-
title: z.string().min(1).optional()
|
|
81
|
-
});
|
|
82
|
-
const documentSchema = z.strictObject({
|
|
83
|
-
/**
|
|
84
|
-
* Highest visibility level this document may include. Defaults to `cv`,
|
|
85
|
-
* because a CV is the reason the `cv` level exists; use `private` for an
|
|
86
|
-
* application containing a cover letter, address or referees.
|
|
87
|
-
*
|
|
88
|
-
* There is no "publish this PDF" option: PDFs are never part of the website
|
|
89
|
-
* build (see `vitakit-check`), and the website renders CV data directly.
|
|
90
|
-
*/
|
|
91
|
-
visibility: z.enum(["public", "cv", "private"]).default("cv"),
|
|
92
|
-
sections: z.record(z.string(), sectionOptions).default({}),
|
|
93
|
-
publications: z.strictObject({
|
|
94
|
-
groups: z.array(z.string()).optional()
|
|
95
|
-
}).default({})
|
|
96
|
-
});
|
|
97
|
-
const DEFAULTS = documentSchema.parse({});
|
|
98
|
-
async function discoverDocuments(cvDir) {
|
|
99
|
-
const files = (await readdir(cvDir).catch(() => fail("cv-tex/", "directory is missing"))).filter((name) => name.endsWith(".tex")).sort();
|
|
100
|
-
const documents = [];
|
|
101
|
-
for (const file of files) {
|
|
102
|
-
const name = basename(file, ".tex");
|
|
103
|
-
const source = await readFile(join(cvDir, file), "utf8");
|
|
104
|
-
if (!/\\begin\{document\}/.test(source)) continue;
|
|
105
|
-
const optionsFile = join(cvDir, `${name}.yml`);
|
|
106
|
-
const raw = await readFile(optionsFile, "utf8").catch(() => void 0);
|
|
107
|
-
let options = DEFAULTS;
|
|
108
|
-
if (raw !== void 0) {
|
|
109
|
-
const parsed = documentSchema.safeParse(parse(raw) ?? {});
|
|
110
|
-
if (!parsed.success) fail(`cv-tex/${name}.yml`, `invalid
|
|
111
|
-
${formatZodIssues(parsed.error)}`);
|
|
112
|
-
options = parsed.data;
|
|
113
|
-
}
|
|
114
|
-
documents.push({ name, texFile: join(cvDir, file), options });
|
|
115
|
-
}
|
|
116
|
-
if (!documents.length) fail("cv-tex/", "no .tex file contains \\begin{document}");
|
|
117
|
-
return documents;
|
|
118
|
-
}
|
|
119
|
-
function valueOf(entry, field) {
|
|
120
|
-
if (field === "year") return entry.date?.from?.year;
|
|
121
|
-
const value = entry[field];
|
|
122
|
-
if (typeof value === "object" && value !== null && "text" in value) return value.text;
|
|
123
|
-
if (typeof value === "object" && value !== null && "name" in value) return value.name;
|
|
124
|
-
return value;
|
|
125
|
-
}
|
|
126
|
-
function selectEntries(entries, options, sectionId) {
|
|
127
|
-
const section = options.sections[sectionId] ?? { where: {} };
|
|
128
|
-
let result = entries.filter((entry) => atMost(entry.visibility, options.visibility));
|
|
129
|
-
if (section.since !== void 0) {
|
|
130
|
-
result = result.filter((entry) => Number(valueOf(entry, "year") ?? 0) >= section.since);
|
|
131
|
-
}
|
|
132
|
-
if (section.until !== void 0) {
|
|
133
|
-
result = result.filter((entry) => Number(valueOf(entry, "year") ?? Infinity) <= section.until);
|
|
134
|
-
}
|
|
135
|
-
for (const [field, expected] of Object.entries(section.where ?? {})) {
|
|
136
|
-
const wanted = new Set(Array.isArray(expected) ? expected : [expected]);
|
|
137
|
-
result = result.filter((entry) => wanted.has(String(valueOf(entry, field) ?? "")));
|
|
138
|
-
}
|
|
139
|
-
return section.limit ? result.slice(0, section.limit) : result;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
const bundledTexDir = resolveAsset("tex");
|
|
143
|
-
const eta = new Eta({ autoEscape: false, autoTrim: [false, false] });
|
|
144
|
-
function tidy(body) {
|
|
145
|
-
return body.replace(/[ \t]+$/gm, "").replace(/\n{2,}/g, "\n").trim();
|
|
146
|
-
}
|
|
147
|
-
const SITE_PATH_HELP = "is a path on the website, and the PDF can only link it with the address the site is published at: add it to content/profile.yml, e.g. `url: https://jane.example.org/`";
|
|
148
|
-
function helpers(where, source) {
|
|
149
|
-
const tex = (value) => value === void 0 || value === null ? "" : escapeTex(String(value));
|
|
150
|
-
const rich = (value) => {
|
|
151
|
-
if (value === void 0 || value === null) return "";
|
|
152
|
-
const source2 = richSource(value);
|
|
153
|
-
try {
|
|
154
|
-
return escapeRichTex(source2);
|
|
155
|
-
} catch (error) {
|
|
156
|
-
return fail(where, error instanceof Error ? error.message : String(error));
|
|
157
|
-
}
|
|
158
|
-
};
|
|
159
|
-
const href = (value) => {
|
|
160
|
-
if (isSitePath(value)) fail(source, `${value} ${SITE_PATH_HELP}`);
|
|
161
|
-
return escapeUrl(value);
|
|
162
|
-
};
|
|
163
|
-
const url = (value) => value ? href(String(value)) : "";
|
|
164
|
-
const inst = (value) => {
|
|
165
|
-
const institution = value;
|
|
166
|
-
if (!institution?.name) return "";
|
|
167
|
-
return institution.url ? `\\href{${href(institution.url)}}{${escapeTex(institution.name)}}` : escapeTex(institution.name);
|
|
168
|
-
};
|
|
169
|
-
const linkOrText = (value) => {
|
|
170
|
-
if (typeof value === "string") return escapeTex(value);
|
|
171
|
-
const item = value;
|
|
172
|
-
if (!item?.label) return "";
|
|
173
|
-
return item.url ? `\\href{${href(item.url)}}{${escapeTex(item.label)}}` : escapeTex(item.label);
|
|
174
|
-
};
|
|
175
|
-
const date = (value) => escapeTex(formatDateRange(value));
|
|
176
|
-
const term = (entry) => typeof entry.term === "string" ? escapeTex(entry.term) : date(entry.date);
|
|
177
|
-
const titleOf = (entry) => {
|
|
178
|
-
const field = entryTitleField(entry);
|
|
179
|
-
return field ? entry[field] : "";
|
|
180
|
-
};
|
|
181
|
-
const secondaryOf = (entry) => entrySecondaryParts(entry, entryTitleField(entry)).map((part) => part.kind === "institution" ? inst(part.institution) : escapeTex(part.text)).filter(Boolean).join(", ");
|
|
182
|
-
const bibgroup = (group) => {
|
|
183
|
-
const unsafe = /[\s%#\\{},~]/;
|
|
184
|
-
const bad = [group.id, ...group.entries.map((entry) => entry.key)].find((value) => !value || unsafe.test(value));
|
|
185
|
-
if (bad !== void 0) {
|
|
186
|
-
fail(where, `"${bad}" cannot be used as a citation key or group id in LaTeX; use letters, digits and - _ : . / +`);
|
|
187
|
-
}
|
|
188
|
-
return `\\vitabibgroup{${group.id}}{${group.entries.map((entry) => entry.key).join(",")}}`;
|
|
189
|
-
};
|
|
190
|
-
return { tex, rich, url, inst, linkOrText, date, term, titleOf, secondaryOf, bibgroup };
|
|
191
|
-
}
|
|
192
|
-
async function resolveTemplate(id, userDir) {
|
|
193
|
-
const candidates = [join(userDir, `${id}.eta`), join(bundledTexDir, "templates", `${id}.eta`), join(bundledTexDir, "templates", "_generic.eta")];
|
|
194
|
-
for (const path of candidates) {
|
|
195
|
-
if (existsSync(path)) return { path, source: await readFile(path, "utf8") };
|
|
196
|
-
}
|
|
197
|
-
throw new Error(`vitakit: no template for section ${id}`);
|
|
198
|
-
}
|
|
199
|
-
async function assertTemplatesAreUsed(templateDir, ids) {
|
|
200
|
-
const files = await readdir(templateDir).catch(() => []);
|
|
201
|
-
const known = /* @__PURE__ */ new Set([...ids, "publications", "_generic"]);
|
|
202
|
-
for (const file of files.filter((name) => name.endsWith(".eta")).sort()) {
|
|
203
|
-
const id = file.slice(0, -".eta".length);
|
|
204
|
-
if (known.has(id)) continue;
|
|
205
|
-
const suggestion = ids.find((candidate) => candidate.startsWith(id) || id.startsWith(candidate));
|
|
206
|
-
fail(
|
|
207
|
-
`cv-tex/templates/${file}`,
|
|
208
|
-
`no section is called "${id}", so this template is never used${suggestion ? `; did you mean ${suggestion}.eta?` : ""}
|
|
209
|
-
sections: ${ids.join(", ") || "(none)"}`
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
async function emitDocument(emit) {
|
|
214
|
-
await mkdir(emit.outDir, { recursive: true });
|
|
215
|
-
await assertTemplatesAreUsed(emit.templateDir, Object.keys(emit.sections));
|
|
216
|
-
const written = [];
|
|
217
|
-
for (const [id, entries] of Object.entries(emit.sections)) {
|
|
218
|
-
const where = `cv-tex/generated/${id}.tex`;
|
|
219
|
-
const selected = selectEntries(entries, emit.options, id);
|
|
220
|
-
const { source } = await resolveTemplate(id, emit.templateDir);
|
|
221
|
-
let body;
|
|
222
|
-
try {
|
|
223
|
-
body = tidy(eta.renderString(source, { entries: selected, ...helpers(where, `content/${id}.yml`) }));
|
|
224
|
-
} catch (error) {
|
|
225
|
-
throw new Error(`template ${id}.eta: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
|
|
226
|
-
}
|
|
227
|
-
const file = join(emit.outDir, `${id}.tex`);
|
|
228
|
-
await writeFile(file, `% Generated by vitakit. Do not edit; edit content/${id}.yml instead.
|
|
229
|
-
${body.trim()}
|
|
230
|
-
`);
|
|
231
|
-
written.push(file);
|
|
232
|
-
}
|
|
233
|
-
if (emit.publications.length) {
|
|
234
|
-
const groups = emit.options.publications.groups ? emit.options.publications.groups.map((id) => emit.publications.find((group) => group.id === id)).filter((group) => group !== void 0) : emit.publications;
|
|
235
|
-
if (!emit.siteUrl) {
|
|
236
|
-
const local = groups.flatMap((group) => group.entries).find((entry) => entry.titleUrl && isSitePath(entry.titleUrl));
|
|
237
|
-
if (local) fail("content/publications.bib", `${local.key}: url ${local.titleUrl} ${SITE_PATH_HELP}`);
|
|
238
|
-
}
|
|
239
|
-
const { source } = await resolveTemplate("publications", emit.templateDir);
|
|
240
|
-
const body = tidy(eta.renderString(source, { groups, ...helpers("publications.tex", "content/publications.bib") }));
|
|
241
|
-
const file = join(emit.outDir, "publications.tex");
|
|
242
|
-
await writeFile(file, `% Generated by vitakit. Do not edit; edit content/publications.bib instead.
|
|
243
|
-
${body.trim()}
|
|
244
|
-
`);
|
|
245
|
-
written.push(file);
|
|
246
|
-
}
|
|
247
|
-
return written;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
export { SITE_PATH_HELP as S, escapeTex as a, bundledTexDir as b, escapeUrl as c, discoverDocuments as d, emitDocument as e, documentSchema as f, escapeRichTex as g, selectEntries as s };
|