@anja.codes/vitakit 0.4.0 → 0.5.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/dist/chunks/build.mjs +29 -4
- package/dist/chunks/documents.mjs +7 -0
- package/dist/tex/vitakit-cv.cls +30 -4
- package/dist/tex.d.mts +1 -0
- package/package.json +1 -1
package/dist/chunks/build.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { mkdir, readFile, writeFile, rm, copyFile, rename } from 'node:fs/promises';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import process from 'node:process';
|
|
5
|
-
import { join, delimiter, resolve } from 'node:path';
|
|
5
|
+
import { join, delimiter, resolve, extname } from 'node:path';
|
|
6
6
|
import { l as loadContent } from '../shared/vitakit.DteA9vLy.mjs';
|
|
7
7
|
import { f as fail } from '../shared/vitakit.DCK90zq4.mjs';
|
|
8
8
|
import { discoverDocuments, validateDocumentSections, selectEntries } from './documents.mjs';
|
|
@@ -74,7 +74,7 @@ Full log: ${log}` : ""}`));
|
|
|
74
74
|
});
|
|
75
75
|
});
|
|
76
76
|
}
|
|
77
|
-
function profilePreamble(content, document, lists) {
|
|
77
|
+
function profilePreamble(content, document, lists, cvDir) {
|
|
78
78
|
const { profile } = content;
|
|
79
79
|
const href = (url) => {
|
|
80
80
|
if (isSitePath(url)) fail("content/profile.yml", `links: ${url} ${SITE_PATH_HELP}`);
|
|
@@ -91,6 +91,7 @@ function profilePreamble(content, document, lists) {
|
|
|
91
91
|
`\\vitaname{${escapeTex(profile.name)}}`,
|
|
92
92
|
`\\vitatagline{${tagline}}`,
|
|
93
93
|
`\\vitacontact{${contact}}`,
|
|
94
|
+
...portraitLine(content, document, cvDir),
|
|
94
95
|
...profile.url ? siteUrlSourcemap(profile.url) : [],
|
|
95
96
|
...Object.entries(content.sections).flatMap(([id, entries]) => {
|
|
96
97
|
const title = document.options.sections[id]?.title;
|
|
@@ -103,6 +104,30 @@ function profilePreamble(content, document, lists) {
|
|
|
103
104
|
""
|
|
104
105
|
].join("\n");
|
|
105
106
|
}
|
|
107
|
+
const PORTRAIT_FORMATS = [".jpg", ".jpeg", ".png", ".pdf"];
|
|
108
|
+
function portraitLine(content, document, cvDir) {
|
|
109
|
+
const option = document.options.portrait;
|
|
110
|
+
if (option === false) return [];
|
|
111
|
+
const where = `cv-tex/${document.name}.yml`;
|
|
112
|
+
const path = option === true ? content.profile.avatar : option;
|
|
113
|
+
if (!path) {
|
|
114
|
+
fail(where, "portrait: true shows avatar from content/profile.yml, which has none; add one there, or give the image here, e.g. portrait: /img/portrait.jpg");
|
|
115
|
+
}
|
|
116
|
+
const what = option === true ? `portrait: true shows avatar ${path} (content/profile.yml)` : `portrait: ${path}`;
|
|
117
|
+
const format = extname(path).toLowerCase();
|
|
118
|
+
if (!PORTRAIT_FORMATS.includes(format)) {
|
|
119
|
+
fail(where, `${what}, but LaTeX cannot include ${format ? `a ${format} file` : "a file without an extension"}; use a JPG, PNG or PDF${option === true ? ", e.g. portrait: /img/portrait.jpg" : ""}`);
|
|
120
|
+
}
|
|
121
|
+
if (!/^[\w./-]+$/.test(path) || path.split("/").includes("..")) {
|
|
122
|
+
fail(where, `${what}: the path may only contain letters, digits and - . _ /`);
|
|
123
|
+
}
|
|
124
|
+
if (!existsSync(join(cvDir, "..", "public", path))) fail(where, `${what}: public${path} does not exist`);
|
|
125
|
+
const ejected = join(cvDir, CLASS_FILE);
|
|
126
|
+
if (existsSync(ejected) && !readFileSync(ejected, "utf8").includes("\\vitaportrait")) {
|
|
127
|
+
fail(where, `portrait needs \\vitaportrait, which cv-tex/${CLASS_FILE} does not define: it was ejected from an earlier vitakit. Run vitakit eject vitakit-cv.cls --force for the current class (your copy is kept as a backup), or remove portrait`);
|
|
128
|
+
}
|
|
129
|
+
return [`\\vitaportrait{../public${path}}`];
|
|
130
|
+
}
|
|
106
131
|
function siteUrlSourcemap(siteUrl) {
|
|
107
132
|
const base = siteUrl.replace(/\/?$/, "/");
|
|
108
133
|
if (!/^https?:\/\/[\w.:/-]+$/.test(base)) {
|
|
@@ -125,7 +150,7 @@ async function buildDocument(document, content, options, cvDir) {
|
|
|
125
150
|
templateDir: join(cvDir, "templates"),
|
|
126
151
|
outDir
|
|
127
152
|
});
|
|
128
|
-
await writeFile(join(outDir, "profile.tex"), profilePreamble(content, document, lists));
|
|
153
|
+
await writeFile(join(outDir, "profile.tex"), profilePreamble(content, document, lists, cvDir));
|
|
129
154
|
const generatedClass = join(outDir, CLASS_FILE);
|
|
130
155
|
if (existsSync(join(cvDir, CLASS_FILE))) await rm(generatedClass, { force: true });
|
|
131
156
|
else await copyFile(join(bundledTexDir, CLASS_FILE), generatedClass);
|
|
@@ -38,6 +38,13 @@ const documentSchema = z.strictObject({
|
|
|
38
38
|
* build (see `vitakit verify`), and the website renders CV data directly.
|
|
39
39
|
*/
|
|
40
40
|
visibility: z.enum(["public", "cv", "private"]).default("cv"),
|
|
41
|
+
/**
|
|
42
|
+
* A portrait beside the header: `true` for `avatar` from
|
|
43
|
+
* `content/profile.yml`, or a path below `public/` for another image, e.g.
|
|
44
|
+
* a print version. Off by default: whether a CV shows a photo depends on
|
|
45
|
+
* where it is sent, so each document decides.
|
|
46
|
+
*/
|
|
47
|
+
portrait: z.union([z.boolean(), z.string().startsWith("/")]).default(false),
|
|
41
48
|
sections: z.record(z.string(), sectionOptions).default({})
|
|
42
49
|
});
|
|
43
50
|
const DEFAULTS = documentSchema.parse({});
|
package/dist/tex/vitakit-cv.cls
CHANGED
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
\RequirePackage{xcolor}
|
|
28
28
|
\RequirePackage{titlesec}
|
|
29
29
|
\RequirePackage{amsmath,amssymb}
|
|
30
|
+
\RequirePackage{graphicx}
|
|
30
31
|
\RequirePackage{hyperref}
|
|
31
32
|
|
|
32
33
|
%% --- bibliography ---------------------------------------------------------
|
|
@@ -76,13 +77,38 @@
|
|
|
76
77
|
\newcommand{\vitaname}[1]{\def\vita@name{#1}}
|
|
77
78
|
\newcommand{\vitatagline}[1]{\def\vita@tagline{#1}}
|
|
78
79
|
\newcommand{\vitacontact}[1]{\def\vita@contact{#1}}
|
|
79
|
-
|
|
80
|
+
%% `portrait` in the document's .yml: the image, relative to cv-tex/.
|
|
81
|
+
\newcommand{\vitaportrait}[1]{\def\vita@portrait{#1}}
|
|
82
|
+
\vitaname{} \vitatagline{} \vitacontact{} \vitaportrait{}
|
|
80
83
|
|
|
81
|
-
\
|
|
82
|
-
|
|
84
|
+
%% The portrait's height in \vitaheader. It is never wider than a quarter of
|
|
85
|
+
%% the line. \setlength{\vitaportraitheight}{4cm} in preamble.tex changes it.
|
|
86
|
+
\newlength{\vitaportraitheight}
|
|
87
|
+
\setlength{\vitaportraitheight}{3cm}
|
|
88
|
+
\newsavebox{\vita@portraitbox}
|
|
89
|
+
|
|
90
|
+
\newcommand{\vita@headertext}{%
|
|
83
91
|
\noindent{\LARGE\bfseries\vita@name}\par
|
|
84
92
|
\ifx\vita@tagline\empty\else\vspace{0.2em}\noindent{\color{vitamuted}\vita@tagline}\par\fi
|
|
85
|
-
\ifx\vita@contact\empty\else\vspace{0.2em}\noindent{\small\vita@contact}\par\fi
|
|
93
|
+
\ifx\vita@contact\empty\else\vspace{0.2em}\noindent{\small\vita@contact}\par\fi}
|
|
94
|
+
|
|
95
|
+
%% Name, tagline and contact line; with a portrait, those on the left and the
|
|
96
|
+
%% portrait on the right, both aligned at the top.
|
|
97
|
+
\newcommand{\vitaheader}{%
|
|
98
|
+
\begingroup
|
|
99
|
+
\ifx\vita@portrait\empty
|
|
100
|
+
\vita@headertext
|
|
101
|
+
\else
|
|
102
|
+
\sbox{\vita@portraitbox}{\includegraphics[height=\vitaportraitheight,
|
|
103
|
+
width=0.25\linewidth, keepaspectratio]{\vita@portrait}}%
|
|
104
|
+
\noindent
|
|
105
|
+
\begin{minipage}[t]{\dimexpr\linewidth-\wd\vita@portraitbox-1.5em\relax}
|
|
106
|
+
\vspace{0pt}\vita@headertext
|
|
107
|
+
\end{minipage}\hfill
|
|
108
|
+
\begin{minipage}[t]{\wd\vita@portraitbox}
|
|
109
|
+
\vspace{0pt}\usebox{\vita@portraitbox}
|
|
110
|
+
\end{minipage}\par
|
|
111
|
+
\fi
|
|
86
112
|
\vspace{1em}\hrule\vspace{1.5em}
|
|
87
113
|
\endgroup}
|
|
88
114
|
|
package/dist/tex.d.mts
CHANGED
|
@@ -59,6 +59,7 @@ declare const documentSchema: z.ZodObject<{
|
|
|
59
59
|
cv: "cv";
|
|
60
60
|
private: "private";
|
|
61
61
|
}>>;
|
|
62
|
+
portrait: z.ZodDefault<z.ZodUnion<readonly [z.ZodBoolean, z.ZodString]>>;
|
|
62
63
|
sections: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
63
64
|
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]>>]>>>;
|
|
64
65
|
limit: z.ZodOptional<z.ZodNumber>;
|