@anja.codes/vitakit 0.1.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/LICENSE +21 -0
- package/README.md +159 -0
- package/dist/check.d.mts +22 -0
- package/dist/check.mjs +196 -0
- package/dist/cli.d.mts +45 -0
- package/dist/cli.mjs +225 -0
- package/dist/index.d.mts +398 -0
- package/dist/index.mjs +21 -0
- package/dist/nuxt.d.mts +15 -0
- package/dist/nuxt.mjs +98 -0
- package/dist/runtime/components/VitaDate.vue +20 -0
- package/dist/runtime/components/VitaEntry.vue +24 -0
- package/dist/runtime/components/VitaIssues.vue +25 -0
- package/dist/runtime/components/VitaLinks.vue +19 -0
- package/dist/runtime/components/VitaMarkdown.vue +14 -0
- package/dist/runtime/components/VitaPublication.vue +30 -0
- package/dist/runtime/components/VitaPublications.vue +39 -0
- package/dist/runtime/components/VitaSection.vue +60 -0
- package/dist/runtime/composables/useVitakit.ts +47 -0
- package/dist/schemas/education.ts +21 -0
- package/dist/schemas/employment.ts +21 -0
- package/dist/schemas/publications.ts +48 -0
- package/dist/schemas/supervision.ts +20 -0
- package/dist/schemas/talks.ts +19 -0
- package/dist/schemas/teaching.ts +19 -0
- package/dist/shared/index.d.mts +68 -0
- package/dist/shared/index.mjs +57 -0
- package/dist/shared/vitakit.BRhjqpAt.mjs +250 -0
- package/dist/shared/vitakit.BlU-p01e.mjs +14 -0
- package/dist/shared/vitakit.BxcnlFGP.mjs +325 -0
- package/dist/shared/vitakit.CypCF-4Q.mjs +503 -0
- package/dist/shared/vitakit.D8WRg_Xx.d.mts +153 -0
- package/dist/styles/vitakit-math.csl +314 -0
- package/dist/tex/templates/_generic.eta +6 -0
- package/dist/tex/templates/education.eta +9 -0
- package/dist/tex/templates/employment.eta +9 -0
- package/dist/tex/templates/publications.eta +7 -0
- package/dist/tex/templates/supervision.eta +4 -0
- package/dist/tex/templates/talks.eta +3 -0
- package/dist/tex/templates/teaching.eta +6 -0
- package/dist/tex/vitakit-cv.cls +119 -0
- package/dist/tex.d.mts +93 -0
- package/dist/tex.mjs +129 -0
- package/package.json +121 -0
- package/src/bin/vitakit-build-cv.mjs +50 -0
- package/src/bin/vitakit-check.mjs +57 -0
- package/src/bin/vitakit.mjs +158 -0
- package/styles/vitakit-math.csl +314 -0
package/dist/tex.mjs
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir, readFile, writeFile, rm, copyFile, rename } from 'node:fs/promises';
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import process from 'node:process';
|
|
5
|
+
import { join, delimiter } from 'node:path';
|
|
6
|
+
import { l as loadContent, a as loadPublicationsSchema } from './shared/vitakit.CypCF-4Q.mjs';
|
|
7
|
+
import { m as fail, n as isSitePath } from './shared/vitakit.BxcnlFGP.mjs';
|
|
8
|
+
import { d as discoverDocuments, e as emitDocument, b as bundledTexDir, a as escapeTex, c as escapeUrl, S as SITE_PATH_HELP } from './shared/vitakit.BRhjqpAt.mjs';
|
|
9
|
+
export { f as documentSchema, g as escapeRichTex, s as selectEntries } from './shared/vitakit.BRhjqpAt.mjs';
|
|
10
|
+
import 'gray-matter';
|
|
11
|
+
import 'yaml';
|
|
12
|
+
import 'unified';
|
|
13
|
+
import 'remark-parse';
|
|
14
|
+
import 'remark-gfm';
|
|
15
|
+
import 'remark-math';
|
|
16
|
+
import 'remark-rehype';
|
|
17
|
+
import 'rehype-katex';
|
|
18
|
+
import 'rehype-raw';
|
|
19
|
+
import 'rehype-stringify';
|
|
20
|
+
import 'unist-util-visit';
|
|
21
|
+
import '@behackl/citation-js-extras';
|
|
22
|
+
import 'rehype-parse';
|
|
23
|
+
import 'rehype-sanitize';
|
|
24
|
+
import 'zod';
|
|
25
|
+
import 'node:url';
|
|
26
|
+
import 'eta';
|
|
27
|
+
import './shared/index.mjs';
|
|
28
|
+
import './shared/vitakit.BlU-p01e.mjs';
|
|
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 };
|
package/package.json
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@anja.codes/vitakit",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Structured CV data in, academic website and LaTeX CV out.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"cv",
|
|
8
|
+
"academic",
|
|
9
|
+
"nuxt",
|
|
10
|
+
"nuxt-module",
|
|
11
|
+
"latex",
|
|
12
|
+
"bibtex",
|
|
13
|
+
"katex",
|
|
14
|
+
"static-site"
|
|
15
|
+
],
|
|
16
|
+
"homepage": "https://github.com/anja-codes/vitakit#readme",
|
|
17
|
+
"bugs": {
|
|
18
|
+
"url": "https://github.com/anja-codes/vitakit/issues"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/anja-codes/vitakit.git"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
25
|
+
"packageManager": "pnpm@10.29.1",
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=22.18"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"types": "./dist/index.d.mts",
|
|
32
|
+
"import": "./dist/index.mjs"
|
|
33
|
+
},
|
|
34
|
+
"./nuxt": {
|
|
35
|
+
"types": "./dist/nuxt.d.mts",
|
|
36
|
+
"import": "./dist/nuxt.mjs"
|
|
37
|
+
},
|
|
38
|
+
"./runtime/*": "./dist/runtime/*",
|
|
39
|
+
"./styles/*": "./dist/styles/*",
|
|
40
|
+
"./tex/*": "./dist/tex/*",
|
|
41
|
+
"./shared": {
|
|
42
|
+
"types": "./dist/shared/index.d.mts",
|
|
43
|
+
"import": "./dist/shared/index.mjs"
|
|
44
|
+
},
|
|
45
|
+
"./tex": {
|
|
46
|
+
"types": "./dist/tex.d.mts",
|
|
47
|
+
"import": "./dist/tex.mjs"
|
|
48
|
+
},
|
|
49
|
+
"./check": {
|
|
50
|
+
"types": "./dist/check.d.mts",
|
|
51
|
+
"import": "./dist/check.mjs"
|
|
52
|
+
},
|
|
53
|
+
"./cli": {
|
|
54
|
+
"types": "./dist/cli.d.mts",
|
|
55
|
+
"import": "./dist/cli.mjs"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"files": [
|
|
59
|
+
"dist",
|
|
60
|
+
"styles",
|
|
61
|
+
"src/bin",
|
|
62
|
+
"README.md",
|
|
63
|
+
"LICENSE"
|
|
64
|
+
],
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "unbuild",
|
|
67
|
+
"prepare": "unbuild",
|
|
68
|
+
"dev": "unbuild --stub",
|
|
69
|
+
"test": "vitest run",
|
|
70
|
+
"test:watch": "vitest",
|
|
71
|
+
"typecheck": "tsc --noEmit",
|
|
72
|
+
"check": "pnpm typecheck && pnpm test && pnpm build && node scripts/check-dist.mjs && pnpm example:check",
|
|
73
|
+
"check:dist": "node scripts/check-dist.mjs",
|
|
74
|
+
"prepublishOnly": "pnpm check",
|
|
75
|
+
"example": "node scripts/generate-example.mjs",
|
|
76
|
+
"example:check": "node scripts/generate-example.mjs --check",
|
|
77
|
+
"check:deps": "node scripts/check-deps.mjs"
|
|
78
|
+
},
|
|
79
|
+
"dependencies": {
|
|
80
|
+
"@behackl/citation-js-extras": "^0.4.0",
|
|
81
|
+
"@types/hast": "^3.0.5",
|
|
82
|
+
"citation-js": "^0.7.22",
|
|
83
|
+
"eta": "^4.6.0",
|
|
84
|
+
"gray-matter": "^4.0.3",
|
|
85
|
+
"rehype-katex": "^7.0.1",
|
|
86
|
+
"rehype-parse": "^9.0.1",
|
|
87
|
+
"rehype-raw": "^7.0.0",
|
|
88
|
+
"rehype-sanitize": "^6.0.0",
|
|
89
|
+
"rehype-stringify": "^10.0.1",
|
|
90
|
+
"remark-gfm": "^4.0.1",
|
|
91
|
+
"remark-math": "^6.0.0",
|
|
92
|
+
"remark-parse": "^11.0.0",
|
|
93
|
+
"remark-rehype": "^11.1.2",
|
|
94
|
+
"unified": "^11.0.5",
|
|
95
|
+
"unist-util-visit": "^5.1.0",
|
|
96
|
+
"yaml": "^2.8.1",
|
|
97
|
+
"zod": "^4.5.4"
|
|
98
|
+
},
|
|
99
|
+
"peerDependencies": {
|
|
100
|
+
"@nuxt/kit": "^4.0.0",
|
|
101
|
+
"nuxt": "^4.0.0"
|
|
102
|
+
},
|
|
103
|
+
"devDependencies": {
|
|
104
|
+
"@nuxt/kit": "^4.5.2",
|
|
105
|
+
"@nuxt/schema": "^4.5.2",
|
|
106
|
+
"@types/node": "^26.4.1",
|
|
107
|
+
"nitropack": "^2.13.4",
|
|
108
|
+
"typescript": "^5.9.3",
|
|
109
|
+
"unbuild": "^3.6.1",
|
|
110
|
+
"vitest": "^5.0.0",
|
|
111
|
+
"vue": "^3.5.42"
|
|
112
|
+
},
|
|
113
|
+
"publishConfig": {
|
|
114
|
+
"access": "public"
|
|
115
|
+
},
|
|
116
|
+
"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"
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Render content/ into cv-tex/generated/ and typeset it.
|
|
4
|
+
*/
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
import { buildCv } from '../../dist/tex.mjs'
|
|
7
|
+
|
|
8
|
+
const HELP = `vitakit-build-cv — build the PDF documents in cv-tex/
|
|
9
|
+
|
|
10
|
+
Usage
|
|
11
|
+
pnpm build:cv [document...] [options]
|
|
12
|
+
|
|
13
|
+
Renders content/ into cv-tex/generated/<document>/, one .tex file per section,
|
|
14
|
+
then runs latexmk. The finished PDF is written next to its source, as
|
|
15
|
+
cv-tex/<document>.pdf; everything else stays in the generated directory.
|
|
16
|
+
|
|
17
|
+
pnpm build:cv every document in cv-tex/
|
|
18
|
+
pnpm build:cv cv just cv.tex
|
|
19
|
+
pnpm build:cv cv application several
|
|
20
|
+
|
|
21
|
+
Options
|
|
22
|
+
--no-pdf write the .tex files but do not run latexmk. Useful without a
|
|
23
|
+
TeX installation, and to inspect what was generated.
|
|
24
|
+
--help this text
|
|
25
|
+
|
|
26
|
+
A document is a .tex file you own; a copy of cv.tex is a new document. What it
|
|
27
|
+
contains is decided by an optional cv-tex/<document>.yml (visibility, which
|
|
28
|
+
entries, how many). See docs/cv-documents.md.
|
|
29
|
+
|
|
30
|
+
Requires latexmk and biber, from any current TeX distribution.`
|
|
31
|
+
|
|
32
|
+
const args = process.argv.slice(2)
|
|
33
|
+
|
|
34
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
35
|
+
console.log(HELP)
|
|
36
|
+
process.exit(0)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const noPdf = args.includes('--no-pdf')
|
|
40
|
+
const only = args.filter(argument => !argument.startsWith('-'))
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const outputs = await buildCv({ rootDir: process.cwd(), only, noPdf })
|
|
44
|
+
console.log(noPdf
|
|
45
|
+
? `vitakit: generated .tex for ${only.length ? only.join(', ') : 'all documents'}`
|
|
46
|
+
: `vitakit: built ${outputs.join(', ')}`)
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error(`\nvitakit: ${error instanceof Error ? error.message : String(error)}\n`)
|
|
49
|
+
process.exit(1)
|
|
50
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Assert that the generated site is complete and correct.
|
|
4
|
+
*/
|
|
5
|
+
import process from 'node:process'
|
|
6
|
+
import { checkStatic } from '../../dist/check.mjs'
|
|
7
|
+
|
|
8
|
+
const HELP = `vitakit-check — verify the generated site
|
|
9
|
+
|
|
10
|
+
Usage
|
|
11
|
+
pnpm check:static [--out <dir>]
|
|
12
|
+
|
|
13
|
+
Run after \`nuxt generate\`; \`pnpm check\` does both. Checks the built HTML,
|
|
14
|
+
not your content, because a schema cannot catch a renderer that silently did
|
|
15
|
+
nothing:
|
|
16
|
+
|
|
17
|
+
- every page in content/pages/ was generated
|
|
18
|
+
- mathematics was typeset, and KaTeX reported no errors
|
|
19
|
+
- the site was built for the deployment base path (NUXT_APP_BASE_URL), and
|
|
20
|
+
every link and asset (href, src, srcset, poster, …) stays inside it
|
|
21
|
+
- every link into the site leads to a file or page that was generated,
|
|
22
|
+
e.g. slides: /slides/talk.pdf to public/slides/talk.pdf
|
|
23
|
+
- no entry marked \`visibility: cv\` or \`private\` appears anywhere in the site
|
|
24
|
+
- the bibliography is fully formatted wherever it is shown
|
|
25
|
+
|
|
26
|
+
Options
|
|
27
|
+
--out <dir> the generated site; defaults to .output/public
|
|
28
|
+
--no-leak-check skip the check for cv and private entries. It compares their
|
|
29
|
+
titles with the text of every page, so a title that is
|
|
30
|
+
legitimately public elsewhere looks like a leak.
|
|
31
|
+
--help this text
|
|
32
|
+
|
|
33
|
+
Reports every problem and exits non-zero if there is one, so it can gate a
|
|
34
|
+
deployment.`
|
|
35
|
+
|
|
36
|
+
const args = process.argv.slice(2)
|
|
37
|
+
|
|
38
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
39
|
+
console.log(HELP)
|
|
40
|
+
process.exit(0)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const outIndex = args.indexOf('--out')
|
|
44
|
+
|
|
45
|
+
const { checked, failures } = await checkStatic({
|
|
46
|
+
rootDir: process.cwd(),
|
|
47
|
+
outDir: outIndex === -1 ? undefined : args[outIndex + 1],
|
|
48
|
+
skipLeakCheck: args.includes('--no-leak-check'),
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
for (const line of checked) console.log(` ok ${line}`)
|
|
52
|
+
if (failures.length) {
|
|
53
|
+
console.error(`\nvitakit-check: ${failures.length} problem(s):`)
|
|
54
|
+
for (const failure of failures) console.error(` \u2716 ${failure}`)
|
|
55
|
+
process.exit(1)
|
|
56
|
+
}
|
|
57
|
+
console.log('\nvitakit-check: generated site looks correct.')
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The vitakit command. Three subcommands, each of which only writes files you
|
|
4
|
+
* could have written yourself — see docs/cli.md for why there are not more.
|
|
5
|
+
*/
|
|
6
|
+
import process from 'node:process'
|
|
7
|
+
import { relative } from 'node:path'
|
|
8
|
+
import { addSection, eject, listEjectable } from '../../dist/cli.mjs'
|
|
9
|
+
|
|
10
|
+
const USAGE = `vitakit — files for a vitakit project
|
|
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
|
+
}
|