@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.
@@ -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({});
@@ -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
- \vitaname{} \vitatagline{} \vitacontact{}
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
- \newcommand{\vitaheader}{%
82
- \begingroup
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>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anja.codes/vitakit",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Structured CV data in, academic website and LaTeX CV out.",
5
5
  "license": "MIT",
6
6
  "keywords": [