@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.
Files changed (63) hide show
  1. package/README.md +51 -131
  2. package/dist/check.mjs +12 -5
  3. package/dist/chunks/authoring.mjs +367 -0
  4. package/dist/chunks/build.mjs +176 -0
  5. package/dist/chunks/documents.mjs +143 -0
  6. package/dist/chunks/draft.mjs +222 -0
  7. package/dist/chunks/eject.mjs +59 -0
  8. package/dist/chunks/emit.mjs +200 -0
  9. package/dist/chunks/eta.mjs +16 -0
  10. package/dist/chunks/fields.mjs +79 -0
  11. package/dist/chunks/inventory.mjs +199 -0
  12. package/dist/chunks/plan.mjs +144 -0
  13. package/dist/chunks/prompt.mjs +33 -0
  14. package/dist/chunks/schema-source.mjs +12 -0
  15. package/dist/chunks/schemas.mjs +109 -0
  16. package/dist/chunks/sections.mjs +109 -0
  17. package/dist/chunks/snippets.mjs +199 -0
  18. package/dist/chunks/validate.mjs +124 -0
  19. package/dist/chunks/vue.mjs +116 -0
  20. package/dist/cli.d.mts +33 -40
  21. package/dist/cli.mjs +7 -224
  22. package/dist/index.d.mts +372 -42
  23. package/dist/index.mjs +10 -4
  24. package/dist/nuxt.mjs +35 -5
  25. package/dist/runtime/components/VitaSection.vue +94 -38
  26. package/dist/schemas/education.ts +4 -4
  27. package/dist/schemas/employment.ts +3 -3
  28. package/dist/schemas/publications.ts +2 -2
  29. package/dist/schemas/supervision.ts +3 -3
  30. package/dist/schemas/talks.ts +1 -1
  31. package/dist/schemas/teaching.ts +3 -3
  32. package/dist/shared/index.d.mts +60 -9
  33. package/dist/shared/index.mjs +3 -57
  34. package/dist/shared/vitakit.BZZIsWlY.mjs +179 -0
  35. package/dist/shared/vitakit.BerUaxtF.mjs +121 -0
  36. package/dist/shared/{vitakit.D8WRg_Xx.d.mts → vitakit.BfpxDtz1.d.mts} +9 -8
  37. package/dist/shared/{vitakit.CypCF-4Q.mjs → vitakit.By5VWQ1n.mjs} +12 -78
  38. package/dist/shared/vitakit.CfB3hMHl.mjs +60 -0
  39. package/dist/shared/vitakit.CoWX2Z4R.mjs +1554 -0
  40. package/dist/shared/{vitakit.BlU-p01e.mjs → vitakit.CtChKO0I.mjs} +1 -1
  41. package/dist/shared/vitakit.D-DdJaDZ.mjs +20 -0
  42. package/dist/shared/vitakit.D10HyDhF.mjs +19 -0
  43. package/dist/shared/vitakit.DQ6PmGoL.mjs +34 -0
  44. package/dist/shared/vitakit.DbSeENVS.mjs +58 -0
  45. package/dist/shared/vitakit.DcQOdM4I.mjs +57 -0
  46. package/dist/shared/vitakit.Do5HC_MD.mjs +142 -0
  47. package/dist/shared/vitakit.TSMTC5Jl.mjs +6 -0
  48. package/dist/tex/templates/_generic.eta +6 -4
  49. package/dist/tex/templates/education.eta +10 -5
  50. package/dist/tex/templates/employment.eta +12 -5
  51. package/dist/tex/templates/publications.eta +4 -2
  52. package/dist/tex/templates/supervision.eta +10 -2
  53. package/dist/tex/templates/talks.eta +5 -1
  54. package/dist/tex/templates/teaching.eta +6 -4
  55. package/dist/tex/vitakit-cv.cls +21 -6
  56. package/dist/tex.d.mts +53 -5
  57. package/dist/tex.mjs +20 -113
  58. package/package.json +10 -7
  59. package/src/bin/vitakit.mjs +3 -153
  60. package/dist/shared/vitakit.BRhjqpAt.mjs +0 -250
  61. package/dist/shared/vitakit.BxcnlFGP.mjs +0 -325
  62. package/src/bin/vitakit-build-cv.mjs +0 -50
  63. package/src/bin/vitakit-check.mjs +0 -57
@@ -0,0 +1,121 @@
1
+ import { z } from 'zod';
2
+ import { f as fail } from './vitakit.DbSeENVS.mjs';
3
+ import { i as isSitePath, s as sitePathMarker, P as PATH_KEY } from './vitakit.D10HyDhF.mjs';
4
+
5
+ const MARKER_KEY = "__vitakitRich";
6
+ function isRichMarker(value) {
7
+ return typeof value === "object" && value !== null && MARKER_KEY in value;
8
+ }
9
+ function richMarker(value) {
10
+ return { [MARKER_KEY]: value };
11
+ }
12
+ function renderRichFields(value, renderer, path, where) {
13
+ if (isRichMarker(value)) {
14
+ try {
15
+ return { text: value[MARKER_KEY], html: renderer.inline(value[MARKER_KEY]) };
16
+ } catch (error) {
17
+ fail(where, `${path}: ${error instanceof Error ? error.message : String(error)}`);
18
+ }
19
+ }
20
+ if (Array.isArray(value)) {
21
+ return value.map((item, index) => renderRichFields(item, renderer, `${path}[${index}]`, where));
22
+ }
23
+ if (typeof value === "object" && value !== null) {
24
+ return Object.fromEntries(Object.entries(value).map(
25
+ ([key, item]) => [key, renderRichFields(item, renderer, path ? `${path}.${key}` : key, where)]
26
+ ));
27
+ }
28
+ return value;
29
+ }
30
+
31
+ const text = z.string().trim().min(1);
32
+ const richText = text.transform((value) => richMarker(value));
33
+ const absoluteUrl = z.string().trim().refine(
34
+ // One check, not `.url().refine(...)`: zod runs a refinement even after
35
+ // `.url()` failed, and `new URL()` then throws instead of reporting.
36
+ (value) => URL.canParse(value) && ["http:", "https:", "mailto:"].includes(new URL(value).protocol),
37
+ "expected an http(s) or mailto URL"
38
+ );
39
+ const url = z.string().trim().transform((value, context) => {
40
+ if (isSitePath(value) && !/\s/.test(value)) return sitePathMarker(value);
41
+ const parsed = absoluteUrl.safeParse(value);
42
+ if (parsed.success) return parsed.data;
43
+ context.addIssue({ code: "custom", message: "expected an http(s) or mailto URL, or a path on the site such as /slides/talk.pdf" });
44
+ return z.NEVER;
45
+ });
46
+ const DATE = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/;
47
+ function parseDatePoint(value, context) {
48
+ const match = DATE.exec(value);
49
+ if (!match) {
50
+ context.addIssue({ code: "custom", message: `expected a date like 2024, 2024-03 or 2024-03-15, got "${value}"` });
51
+ return void 0;
52
+ }
53
+ const [, y, m, d] = match;
54
+ const year = Number(y);
55
+ const month = m === void 0 ? void 0 : Number(m);
56
+ const day = d === void 0 ? void 0 : Number(d);
57
+ if (month !== void 0 && (month < 1 || month > 12)) {
58
+ context.addIssue({ code: "custom", message: `month out of range in "${value}"` });
59
+ return void 0;
60
+ }
61
+ if (day !== void 0) {
62
+ const date = new Date(Date.UTC(year, month - 1, day));
63
+ if (date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
64
+ context.addIssue({ code: "custom", message: `"${value}" is not a real date` });
65
+ return void 0;
66
+ }
67
+ }
68
+ return {
69
+ raw: value,
70
+ year,
71
+ month,
72
+ day,
73
+ // Pad missing parts so that 2024 < 2024-03 < 2024-03-15 sorts correctly.
74
+ sortKey: `${String(year).padStart(4, "0")}-${String(month ?? 1).padStart(2, "0")}-${String(day ?? 1).padStart(2, "0")}`
75
+ };
76
+ }
77
+ const datePoint = z.union([z.string(), z.number().int()]).transform((value) => String(value));
78
+ const dateRange = z.union([
79
+ datePoint,
80
+ z.strictObject({
81
+ from: datePoint,
82
+ to: z.union([datePoint, z.literal("present")]).optional()
83
+ })
84
+ ]).transform((value, context) => {
85
+ const source = typeof value === "string" ? { from: value, to: void 0 } : value;
86
+ const from = parseDatePoint(source.from, context);
87
+ const ongoing = source.to === "present";
88
+ const to = source.to === void 0 || ongoing ? void 0 : parseDatePoint(source.to, context);
89
+ if (!from || source.to !== void 0 && !ongoing && !to) return z.NEVER;
90
+ if (to && to.sortKey < from.sortKey) {
91
+ context.addIssue({ code: "custom", message: `range ends (${to.raw}) before it starts (${from.raw})` });
92
+ return z.NEVER;
93
+ }
94
+ return { from, to, ongoing, point: typeof value === "string" };
95
+ });
96
+ function urlLabel(value) {
97
+ return typeof value === "string" ? value.replace(/^(?:https?:\/\/|mailto:)/, "").replace(/\/$/, "") : value[PATH_KEY];
98
+ }
99
+ const linked = z.union([
100
+ url.transform((value) => ({ label: urlLabel(value), url: value })),
101
+ text.transform((label) => ({ label })),
102
+ z.strictObject({ label: text, url: url.optional() }),
103
+ z.strictObject({ name: text, url: url.optional() }).transform(({ name, url: url2 }) => ({ label: name, url: url2 }))
104
+ ]).transform((value) => {
105
+ const target = "url" in value ? value.url : void 0;
106
+ return target === void 0 ? { label: value.label } : { label: value.label, url: target };
107
+ });
108
+ const requiredLink = linked.transform((value, context) => {
109
+ if (value.url !== void 0) return value;
110
+ context.addIssue({ code: "custom", message: "expected a URL, or { label, url }" });
111
+ return z.NEVER;
112
+ });
113
+ const visibility = z.enum(["public", "cv", "private"]).default("public");
114
+ function entry(shape) {
115
+ return z.strictObject({ visibility, ...shape });
116
+ }
117
+ function section(shape) {
118
+ return z.array(entry(shape));
119
+ }
120
+
121
+ export { MARKER_KEY as M, richText as a, absoluteUrl as b, renderRichFields as c, dateRange as d, entry as e, linked as l, requiredLink as r, section as s, text as t, url as u, visibility as v };
@@ -31,7 +31,7 @@ interface DateRange {
31
31
  * Intermediate form of a `richText` field, before rendering.
32
32
  *
33
33
  * Internal: it exists only between validation and `renderRichFields`, never in
34
- * a `VitakitContent`. `vitakit-check` fails the build if one reaches the output.
34
+ * a `VitakitContent`. `vitakit verify` fails the build if one reaches the output.
35
35
  * Read rich fields with `richSource` / `richHtml` instead of inspecting shapes.
36
36
  */
37
37
  interface RichTextMarker {
@@ -44,14 +44,15 @@ interface RichText {
44
44
  /** Rendered inline HTML; use with `v-html`. */
45
45
  html: string;
46
46
  }
47
- interface Link {
47
+ /** A label that may link: an institution, a funder, a person. See `linked`. */
48
+ interface Linked {
48
49
  label: string;
49
- url: string;
50
- }
51
- interface Institution {
52
- name: string;
53
50
  url?: string;
54
51
  }
52
+ /** A label that always links. */
53
+ interface Link extends Linked {
54
+ url: string;
55
+ }
55
56
  /** Fields every section entry has, whatever else its schema adds. */
56
57
  interface EntryBase {
57
58
  /** Stable identity for `v-for` keys: the source file plus the index. */
@@ -117,7 +118,7 @@ interface Page {
117
118
  interface Profile {
118
119
  name: string;
119
120
  title?: string;
120
- affiliation?: Institution;
121
+ affiliation?: Linked;
121
122
  email?: string;
122
123
  links: Link[];
123
124
  /** Short plain-text description used for page metadata, not rendered. */
@@ -150,4 +151,4 @@ interface VitakitContent {
150
151
  issues: ContentIssue[];
151
152
  }
152
153
 
153
- export type { ContentIssue as C, DateRange as D, Entry as E, Institution as I, Link as L, Page as P, RichTextMarker as R, Visibility as V, VitakitContent as a, DatePoint as b, EntryBase as c, Profile as d, Publication as e, PublicationBadge as f, PublicationGroup as g, RichText as h };
154
+ export type { ContentIssue as C, DateRange as D, Entry as E, Link as L, Page as P, RichTextMarker as R, Visibility as V, VitakitContent as a, DatePoint as b, EntryBase as c, Linked as d, Profile as e, Publication as f, PublicationBadge as g, PublicationGroup as h, RichText as i };
@@ -11,12 +11,16 @@ import rehypeKatex from 'rehype-katex';
11
11
  import rehypeRaw from 'rehype-raw';
12
12
  import rehypeStringify from 'rehype-stringify';
13
13
  import { visit } from 'unist-util-visit';
14
- import { m as fail, p as formatZodIssues, o as resolveAsset, q as importProjectModule, s as schemas, k as atMost, w as renderRichFields, x as resolveSitePaths, I as Issues, y as profile, z as absoluteSitePath, A as whereOf, B as messageOf, C as page } from './vitakit.BxcnlFGP.mjs';
14
+ import { r as resolveSitePaths, a as absoluteSitePath } from './vitakit.D10HyDhF.mjs';
15
15
  import { existsSync } from 'node:fs';
16
+ import { r as resolveAsset } from './vitakit.DQ6PmGoL.mjs';
16
17
  import { badgePresets, Bibliography } from '@behackl/citation-js-extras';
17
18
  import rehypeParse from 'rehype-parse';
18
19
  import rehypeSanitize, { defaultSchema } from 'rehype-sanitize';
20
+ import { f as fail, a as formatZodIssues, i as importProjectModule, I as Issues, w as whereOf, m as messageOf } from './vitakit.DbSeENVS.mjs';
19
21
  import { z } from 'zod';
22
+ import { findSectionFiles, loadSection, resolveSectionSchema } from '../chunks/sections.mjs';
23
+ import { profile, page } from '../chunks/schemas.mjs';
20
24
 
21
25
  const URL_PROPERTIES = ["href", "src", "poster", "data", "action", "formAction"];
22
26
  const isRootRelative = (value) => value.startsWith("/") && !value.startsWith("//");
@@ -81,7 +85,7 @@ ${details}`);
81
85
  }
82
86
  function inline(source) {
83
87
  const html = markdown(source);
84
- const unwrapped = /^<p>([\s\S]*)<\/p>$/.exec(html);
88
+ const unwrapped = /^<p>((?:(?!<\/p>)[\s\S])*)<\/p>$/.exec(html);
85
89
  if (!unwrapped) throw new Error(`expected a single paragraph, got: ${html}`);
86
90
  return unwrapped[1];
87
91
  }
@@ -305,77 +309,6 @@ async function loadPublications(bibPath, renderer, schema = defaultPublications,
305
309
  return result;
306
310
  }
307
311
 
308
- const RESERVED = /* @__PURE__ */ new Set(["site", "profile"]);
309
- async function exists(path) {
310
- return stat(path).then(() => true, () => false);
311
- }
312
- async function loadRepoSchema(path, id) {
313
- const { mtimeMs } = await stat(path);
314
- const module = await importProjectModule(path, mtimeMs, `schemas/${id}.ts`);
315
- const schema = module.default ?? module.schema;
316
- if (!schema || typeof schema.safeParse !== "function") {
317
- fail(`schemas/${id}.ts`, "must export a Zod schema as its default export");
318
- }
319
- return schema;
320
- }
321
- const STARTER = (id) => `create schemas/${id}.ts, for example:
322
-
323
- import { z, section, dateRange } from '@anja.codes/vitakit'
324
-
325
- export default section({
326
- date: dateRange,
327
- title: z.string(),
328
- })`;
329
- async function discoverSections(rootDir) {
330
- const contentDir = join(rootDir, "content");
331
- const schemaDir = join(rootDir, "schemas");
332
- const files = (await readdir(contentDir).catch(() => fail("content/", "directory is missing"))).filter((name) => name.endsWith(".yml") || name.endsWith(".yaml")).sort();
333
- const sources = [];
334
- for (const file of files) {
335
- const id = file.replace(/\.ya?ml$/, "");
336
- if (RESERVED.has(id)) continue;
337
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) {
338
- fail(`content/${file}`, "file name must be lowercase kebab-case, e.g. `research-stays.yml`");
339
- }
340
- const override = join(schemaDir, `${id}.ts`);
341
- if (await exists(override)) {
342
- sources.push({ id, file: join(contentDir, file), schemaOrigin: "repo", schema: await loadRepoSchema(override, id) });
343
- continue;
344
- }
345
- const builtin = schemas[id];
346
- if (!builtin) fail(`content/${file}`, `no schema for this file; ${STARTER(id)}`);
347
- sources.push({ id, file: join(contentDir, file), schemaOrigin: "built-in", schema: builtin });
348
- }
349
- return sources;
350
- }
351
- function byDateDesc(a, b) {
352
- const left = a.date?.from?.sortKey;
353
- const right = b.date?.from?.sortKey;
354
- if (left === right) return 0;
355
- if (!left) return 1;
356
- if (!right) return -1;
357
- return right.localeCompare(left);
358
- }
359
- async function loadSection(source, options) {
360
- const where = `content/${source.id}.yml`;
361
- const raw = await readFile(source.file, "utf8");
362
- let data;
363
- try {
364
- data = parse(raw);
365
- } catch (error) {
366
- fail(where, error instanceof Error ? error.message : String(error));
367
- }
368
- if (data === null) return [];
369
- if (!Array.isArray(data)) fail(where, "expected a list of entries");
370
- const parsed = source.schema.safeParse(data);
371
- if (!parsed.success) {
372
- const origin = source.schemaOrigin === "repo" ? ` (schema: schemas/${source.id}.ts)` : "";
373
- fail(where, `invalid${origin}
374
- ${formatZodIssues(parsed.error)}`);
375
- }
376
- return parsed.data.map((item, index) => ({ ...item, id: `${source.id}-${index}` })).filter((item) => atMost(item.visibility, options.maxVisibility)).map((item, index) => renderRichFields(item, options.renderer, String(index), where)).map((item) => resolveSitePaths(item, options.resolvePath ?? ((path) => path))).sort(byDateDesc);
377
- }
378
-
379
312
  const FALLBACK_PROFILE = { name: "Your Name", links: [], nav: [], footerHtml: "" };
380
313
  async function loadYaml(file, where) {
381
314
  const raw = await readFile(file, "utf8").catch(() => fail(where, "missing"));
@@ -462,12 +395,13 @@ async function loadPages(contentDir, renderer, issues) {
462
395
  return pages.sort((a, b) => a.order - b.order || a.route.localeCompare(b.route));
463
396
  }
464
397
  async function loadSections(rootDir, maxVisibility, renderer, resolvePath, issues) {
465
- const sources = await issues.attempt("content/", () => discoverSections(rootDir), []);
398
+ const { files, problems } = await issues.attempt("content/", () => findSectionFiles(rootDir), { files: [], problems: [] });
399
+ for (const problem of problems) issues.record(problem.where, problem.message);
466
400
  const sections = {};
467
- for (const source of sources) {
468
- sections[source.id] = await issues.attempt(
469
- `content/${source.id}.yml`,
470
- () => loadSection(source, { maxVisibility, renderer, resolvePath }),
401
+ for (const file of files) {
402
+ sections[file.id] = await issues.attempt(
403
+ file.where,
404
+ async () => loadSection(await resolveSectionSchema(rootDir, file), { maxVisibility, renderer, resolvePath }),
471
405
  []
472
406
  );
473
407
  }
@@ -0,0 +1,60 @@
1
+ function formatPoint(point, { locale = "en", precision = "auto" }) {
2
+ if (precision === "year" || point.month === void 0) return String(point.year);
3
+ const date = new Date(Date.UTC(point.year, point.month - 1, point.day ?? 1));
4
+ return new Intl.DateTimeFormat(locale, {
5
+ year: "numeric",
6
+ month: "short",
7
+ ...point.day === void 0 ? {} : { day: "numeric" },
8
+ timeZone: "UTC"
9
+ }).format(date);
10
+ }
11
+ function formatDateRange(range, options = {}) {
12
+ if (!range) return "";
13
+ const { presentLabel = "present" } = options;
14
+ const from = formatPoint(range.from, options);
15
+ if (range.point) return from;
16
+ if (range.ongoing) return `${from}\u2013${presentLabel}`;
17
+ if (!range.to) return from;
18
+ const to = formatPoint(range.to, options);
19
+ return from === to ? from : `${from}\u2013${to}`;
20
+ }
21
+ function dateSortKey(range) {
22
+ return range?.from.sortKey ?? "";
23
+ }
24
+
25
+ const TITLE_FIELDS = ["title", "position", "degree", "role", "student"];
26
+ const SECONDARY_FIELDS = ["student", "role", "event", "term", "degree"];
27
+ const LINKED_FIELDS = ["institution", "issuer", "organisation"];
28
+ function entryTitleField(entry, fields = TITLE_FIELDS) {
29
+ return fields.find((field) => {
30
+ const value = entry[field];
31
+ if (typeof value === "string") return value.length > 0;
32
+ return typeof value === "object" && value !== null && "html" in value;
33
+ });
34
+ }
35
+ function entrySecondaryParts(entry, titleField) {
36
+ const parts = [];
37
+ for (const field of SECONDARY_FIELDS) {
38
+ if (field === titleField) continue;
39
+ const value = entry[field];
40
+ if (typeof value === "string" && value.length > 0) parts.push({ kind: "text", text: value });
41
+ }
42
+ for (const field of LINKED_FIELDS) {
43
+ const value = entry[field];
44
+ if (value?.label) {
45
+ parts.push({ kind: "linked", linked: value });
46
+ break;
47
+ }
48
+ }
49
+ if (typeof entry.location === "string" && entry.location.length > 0) {
50
+ parts.push({ kind: "text", text: entry.location });
51
+ }
52
+ return parts;
53
+ }
54
+ function asLinkedList(value) {
55
+ const isLinked = (item) => typeof item === "object" && item !== null && typeof item.label === "string";
56
+ if (Array.isArray(value)) return value.filter(isLinked);
57
+ return isLinked(value) ? [value] : [];
58
+ }
59
+
60
+ export { LINKED_FIELDS as L, SECONDARY_FIELDS as S, TITLE_FIELDS as T, asLinkedList as a, entryTitleField as b, dateSortKey as d, entrySecondaryParts as e, formatDateRange as f };