@anja.codes/vitakit 0.3.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/check.mjs +42 -10
- package/dist/chunks/authoring.mjs +2 -2
- package/dist/chunks/build.mjs +30 -5
- package/dist/chunks/documents.mjs +8 -1
- package/dist/chunks/eject.mjs +1 -1
- package/dist/chunks/emit.mjs +1 -1
- package/dist/chunks/inventory.mjs +1 -1
- package/dist/chunks/migrations.mjs +45 -6
- package/dist/{shared/vitakit.DvvUYUTS.mjs → chunks/placements.mjs} +1 -1
- package/dist/chunks/plan.mjs +1 -1
- package/dist/chunks/props.mjs +50 -0
- package/dist/chunks/snippets.mjs +31 -13
- package/dist/chunks/validate.mjs +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +3 -7
- package/dist/nuxt.mjs +45 -6
- package/dist/runtime/components/VitaSection.vue +13 -11
- package/dist/runtime/composables/useVitakit.ts +3 -3
- package/dist/shared/index.d.mts +4 -4
- package/dist/shared/{vitakit.BfR_mUHm.d.mts → vitakit.CB8b91Lh.d.mts} +37 -1
- package/dist/shared/{vitakit.zwr1ssO5.mjs → vitakit.CvYy_Quj.mjs} +9 -3
- package/dist/tex/vitakit-cv.cls +30 -4
- package/dist/tex.d.mts +1 -0
- package/dist/tex.mjs +1 -1
- package/package.json +1 -1
package/dist/check.mjs
CHANGED
|
@@ -77,18 +77,48 @@ function urlReferences(html) {
|
|
|
77
77
|
}
|
|
78
78
|
return found;
|
|
79
79
|
}
|
|
80
|
-
|
|
80
|
+
function listing(listings, dir) {
|
|
81
|
+
let names = listings.get(dir);
|
|
82
|
+
if (!names) {
|
|
83
|
+
names = readdir(dir, { withFileTypes: true }).then(
|
|
84
|
+
(entries) => new Map(entries.map((entry) => [entry.name, entry.isDirectory()])),
|
|
85
|
+
() => void 0
|
|
86
|
+
);
|
|
87
|
+
listings.set(dir, names);
|
|
88
|
+
}
|
|
89
|
+
return names;
|
|
90
|
+
}
|
|
91
|
+
async function existsInSite(outDir, path, listings) {
|
|
81
92
|
let decoded;
|
|
82
93
|
try {
|
|
83
94
|
decoded = decodeURIComponent(path.replace(/[?#].*$/, ""));
|
|
84
95
|
} catch {
|
|
85
|
-
return false;
|
|
96
|
+
return { found: false };
|
|
97
|
+
}
|
|
98
|
+
if (relative(outDir, join(outDir, decoded)).startsWith("..")) return { found: false };
|
|
99
|
+
const segments = decoded.split("/").filter((segment) => segment !== "" && segment !== ".");
|
|
100
|
+
let dir = outDir;
|
|
101
|
+
const actual = [];
|
|
102
|
+
let caseOnly = false;
|
|
103
|
+
for (const [index, segment] of segments.entries()) {
|
|
104
|
+
const names = await listing(listings, dir);
|
|
105
|
+
if (!names) return { found: false };
|
|
106
|
+
let name = names.has(segment) ? segment : void 0;
|
|
107
|
+
if (name === void 0) {
|
|
108
|
+
const lower = segment.toLowerCase();
|
|
109
|
+
name = [...names.keys()].find((candidate) => candidate.toLowerCase() === lower);
|
|
110
|
+
if (name === void 0) return { found: false };
|
|
111
|
+
caseOnly = true;
|
|
112
|
+
}
|
|
113
|
+
actual.push(name);
|
|
114
|
+
const isDirectory = names.get(name);
|
|
115
|
+
if (index < segments.length - 1 && !isDirectory) return { found: false };
|
|
116
|
+
dir = join(dir, name);
|
|
86
117
|
}
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return found?.isDirectory() === true && (await stat(join(target, "index.html")).catch(() => void 0))?.isFile() === true;
|
|
118
|
+
const last = segments.length ? (await listing(listings, join(dir, "..")))?.get(actual.at(-1)) : true;
|
|
119
|
+
const leads = last === false || (await listing(listings, dir))?.get("index.html") === false;
|
|
120
|
+
if (!leads) return { found: false };
|
|
121
|
+
return caseOnly ? { found: false, differsInCase: `/${actual.join("/")}${decoded.endsWith("/") ? "/" : ""}` } : { found: true };
|
|
92
122
|
}
|
|
93
123
|
const ENTITIES = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: " " };
|
|
94
124
|
function decodeEntity(reference, name) {
|
|
@@ -181,14 +211,16 @@ async function checkStatic(options) {
|
|
|
181
211
|
checked.push(`assets and links use ${base}`);
|
|
182
212
|
if (builtForBase) {
|
|
183
213
|
const missing = /* @__PURE__ */ new Map();
|
|
214
|
+
const listings = /* @__PURE__ */ new Map();
|
|
184
215
|
for (const [file, html] of pages) {
|
|
185
216
|
for (const path of urlReferences(html)) {
|
|
186
217
|
if (!underBase(path, base) || missing.has(path)) continue;
|
|
187
|
-
|
|
218
|
+
const found = await existsInSite(outDir, path.slice(base.length), listings);
|
|
219
|
+
if (!found.found) missing.set(path, { file: relative(outDir, file), differsInCase: found.differsInCase });
|
|
188
220
|
}
|
|
189
221
|
}
|
|
190
|
-
for (const [path, file] of missing) {
|
|
191
|
-
fail(`${file}: ${path} leads nowhere: no such file or page in the generated site. A file belongs in public/, a page in content/pages/ or app/pages/`);
|
|
222
|
+
for (const [path, { file, differsInCase }] of missing) {
|
|
223
|
+
fail(differsInCase !== void 0 ? `${file}: ${path} leads nowhere: the site has ${base}${differsInCase.slice(1)}, which differs only in case. Web servers usually tell them apart, even where your computer does not; write the name as it is` : `${file}: ${path} leads nowhere: no such file or page in the generated site. A file belongs in public/, a page in content/pages/ or app/pages/`);
|
|
192
224
|
}
|
|
193
225
|
checked.push("links into the site lead somewhere");
|
|
194
226
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { parse } from 'yaml';
|
|
2
2
|
import { p as propertyKey, a as addSchemaField, e as ensurePackageImports } from '../shared/vitakit.CCTDg7pz.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import { findPdfPublicationPlacements, findPdfPlacements } from './placements.mjs';
|
|
4
4
|
import { a as escapeTex } from '../shared/vitakit.DcQOdM4I.mjs';
|
|
5
5
|
import { b as insertLinesAfter, i as indentationAt, c as insertLinesBefore } from '../shared/vitakit.UfoXdL7U.mjs';
|
|
6
6
|
import { addPublicationsToPage, publicationsElementSource, addSectionToPage, sectionElementSource } from './vue.mjs';
|
|
7
7
|
import { addNavItem, yamlScalar } from './yaml.mjs';
|
|
8
|
-
import { C as CommandError } from '../shared/vitakit.
|
|
8
|
+
import { C as CommandError } from '../shared/vitakit.CvYy_Quj.mjs';
|
|
9
9
|
import '@babel/parser';
|
|
10
10
|
import 'node:fs/promises';
|
|
11
11
|
import 'node:path';
|
package/dist/chunks/build.mjs
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
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';
|
|
9
|
-
import {
|
|
9
|
+
import { publicationPlacementProblems } from './placements.mjs';
|
|
10
10
|
import { discoverSections } from './sections.mjs';
|
|
11
11
|
import { emitDocument, bundledTexDir, SITE_PATH_HELP } from './emit.mjs';
|
|
12
12
|
import { a as escapeTex, b as escapeUrl } from '../shared/vitakit.DcQOdM4I.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);
|
|
@@ -3,7 +3,7 @@ import { basename, join } from 'node:path';
|
|
|
3
3
|
import { parse } from 'yaml';
|
|
4
4
|
import { z } from 'zod';
|
|
5
5
|
import { f as fail, m as messageOf, a as formatZodIssues } from '../shared/vitakit.DCK90zq4.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { findPdfPublicationPlacements, findPdfPlacements } from './placements.mjs';
|
|
7
7
|
import { a as atMost } from '../shared/vitakit.TSMTC5Jl.mjs';
|
|
8
8
|
import { f as fieldValue, a as filterWhere } from '../shared/vitakit.D-DdJaDZ.mjs';
|
|
9
9
|
import 'node:url';
|
|
@@ -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/chunks/eject.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
|
|
|
2
2
|
import { readdir, readFile } from 'node:fs/promises';
|
|
3
3
|
import { basename, join } from 'node:path';
|
|
4
4
|
import { r as resolveAsset } from '../shared/vitakit.DQ6PmGoL.mjs';
|
|
5
|
-
import { C as CommandError } from '../shared/vitakit.
|
|
5
|
+
import { C as CommandError } from '../shared/vitakit.CvYy_Quj.mjs';
|
|
6
6
|
import 'node:url';
|
|
7
7
|
import 'node:process';
|
|
8
8
|
import 'node:util';
|
package/dist/chunks/emit.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { selectEntries } from './documents.mjs';
|
|
|
11
11
|
import 'node:url';
|
|
12
12
|
import 'yaml';
|
|
13
13
|
import 'zod';
|
|
14
|
-
import '
|
|
14
|
+
import './placements.mjs';
|
|
15
15
|
import '../shared/vitakit.TSMTC5Jl.mjs';
|
|
16
16
|
import '../shared/vitakit.D-DdJaDZ.mjs';
|
|
17
17
|
|
|
@@ -8,7 +8,7 @@ import { describeSection } from './fields.mjs';
|
|
|
8
8
|
import { findSectionFiles, resolveSectionSchema } from './sections.mjs';
|
|
9
9
|
import { schemas } from './schemas.mjs';
|
|
10
10
|
import { findDocuments, documentSectionProblems } from './documents.mjs';
|
|
11
|
-
import {
|
|
11
|
+
import { pageRoute, documentDirective, publicationPlacementProblems, findWebPlacements } from './placements.mjs';
|
|
12
12
|
import { m as messageOf, w as whereOf } from '../shared/vitakit.DCK90zq4.mjs';
|
|
13
13
|
import './bibliography.mjs';
|
|
14
14
|
import '../shared/vitakit.DQ6PmGoL.mjs';
|
|
@@ -7,7 +7,7 @@ import { i as importProjectModule } from '../shared/vitakit.DCK90zq4.mjs';
|
|
|
7
7
|
import { addPublicationList, publicationListYaml } from './yaml.mjs';
|
|
8
8
|
import { r as renamePublicationsSettings } from '../shared/vitakit.CCTDg7pz.mjs';
|
|
9
9
|
import { publicationsElements } from './vue.mjs';
|
|
10
|
-
import {
|
|
10
|
+
import { findPdfPlacements } from './placements.mjs';
|
|
11
11
|
import { LISTS_FILE, LISTS_HEADER } from './publication-lists.mjs';
|
|
12
12
|
import 'node:fs';
|
|
13
13
|
import '../shared/vitakit.DQ6PmGoL.mjs';
|
|
@@ -85,15 +85,30 @@ async function statusOrder(root) {
|
|
|
85
85
|
return Array.isArray(old) ? ids(old) : ids(defaultPublications.statuses);
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
async function
|
|
88
|
+
async function appFiles(root, extension, dir = join(root, "app")) {
|
|
89
89
|
const files = [];
|
|
90
90
|
for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
|
|
91
91
|
const path = join(dir, entry.name);
|
|
92
|
-
if (entry.isDirectory()) files.push(...await
|
|
93
|
-
else if (entry.name.endsWith(
|
|
92
|
+
if (entry.isDirectory()) files.push(...await appFiles(root, extension, path));
|
|
93
|
+
else if (entry.name.endsWith(extension)) files.push(relative(root, path).split(sep).join("/"));
|
|
94
94
|
}
|
|
95
95
|
return files.sort();
|
|
96
96
|
}
|
|
97
|
+
const PUBLICATIONS_TITLE_CSS = `.vita-publications__title {
|
|
98
|
+
@apply text-xl font-semibold;
|
|
99
|
+
}
|
|
100
|
+
.vita-publications__title ~ .vita-publications__group .vita-publications__heading {
|
|
101
|
+
@apply mt-4 text-base;
|
|
102
|
+
}`;
|
|
103
|
+
async function checkStyles(ws, root, notes) {
|
|
104
|
+
const sheets = [];
|
|
105
|
+
for (const path of await appFiles(root, ".css")) sheets.push({ path, source: await ws.read(path) });
|
|
106
|
+
if (sheets.some((sheet) => sheet.source.includes(".vita-publications__title"))) return;
|
|
107
|
+
const styled = sheets.find((sheet) => sheet.source.includes(".vita-publications__heading"));
|
|
108
|
+
if (!styled) return;
|
|
109
|
+
notes.after.push(`By hand: ${styled.path} styles the groups of a publication list but not its title, <VitaPublications title>. New projects have, after .vita-publications__heading:
|
|
110
|
+
${PUBLICATIONS_TITLE_CSS}`);
|
|
111
|
+
}
|
|
97
112
|
async function migrateSchema(ws, notes) {
|
|
98
113
|
const path = "schemas/publications.ts";
|
|
99
114
|
const source = await ws.read(path);
|
|
@@ -104,7 +119,7 @@ async function migrateSchema(ws, notes) {
|
|
|
104
119
|
}
|
|
105
120
|
async function migratePages(root, ws, lists, notes) {
|
|
106
121
|
const statuses = await statusOrder(root);
|
|
107
|
-
for (const path of await
|
|
122
|
+
for (const path of await appFiles(root, ".vue")) {
|
|
108
123
|
const source = await ws.read(path);
|
|
109
124
|
if (/(^|\/)VitaPublications\.vue$/.test(path)) {
|
|
110
125
|
notes.after.push(`By hand: ${path} is your copy of VitaPublications. Its props are now list and title, and it reads usePublicationList(list); \`vitakit eject VitaPublications\` shows vitakit's.`);
|
|
@@ -187,6 +202,7 @@ async function migrateDocuments(root, ws, lists, notes) {
|
|
|
187
202
|
}
|
|
188
203
|
async function migratePublicationLists(root, ws, notes) {
|
|
189
204
|
await migrateSchema(ws, notes);
|
|
205
|
+
await checkStyles(ws, root, notes);
|
|
190
206
|
const source = await ws.read(LISTS_FILE);
|
|
191
207
|
let existing;
|
|
192
208
|
try {
|
|
@@ -214,5 +230,28 @@ ${[...lists.added].map(([other, value]) => publicationListYaml(other, value)).jo
|
|
|
214
230
|
if (source === void 0) ws.create(LISTS_FILE, lines, reason, { show: true });
|
|
215
231
|
else await ws.edit(LISTS_FILE, lines, reason);
|
|
216
232
|
}
|
|
233
|
+
const CAST = /\bentry\.[\w$]+\s+as\s+[A-Z][\w$]*(?:\s*\|\s*undefined)?|\(\s*entry\s+as\s+[A-Z][\w$<>'" |]*\)/;
|
|
234
|
+
async function migrateSectionComponents(root, ws, notes) {
|
|
235
|
+
const { findWebPlacements } = await import('./placements.mjs');
|
|
236
|
+
const { sectionProps, typeSectionProps } = await import('./props.mjs');
|
|
237
|
+
const { placements, components } = await findWebPlacements(root);
|
|
238
|
+
for (const [name, file] of components) {
|
|
239
|
+
const source = await ws.read(file);
|
|
240
|
+
if (source === void 0) continue;
|
|
241
|
+
const found = sectionProps(source);
|
|
242
|
+
if (!found.ok || !found.value.untyped) continue;
|
|
243
|
+
const ids = [...new Set(placements.filter((placement) => placement.component === name).map((placement) => placement.section))].sort();
|
|
244
|
+
if (!ids.length) {
|
|
245
|
+
notes.after.push(`By hand: ${file} is shown on no page with a literal id. To type its entries, give its id the sections it shows, e.g. id: 'talks' | 'talks-public'.`);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
const typed = typeSectionProps(source, ids);
|
|
249
|
+
if (!typed.ok) continue;
|
|
250
|
+
await ws.edit(file, typed.value.source, `its entries are typed by the schema of ${ids.join(", ")}`);
|
|
251
|
+
if (CAST.test(source)) {
|
|
252
|
+
notes.after.push(`${file} casts entries to types of its own (as \u2026). They still work, but its entries are typed by their schema now; the casts can go, and fields read as written, e.g. entry.<group>.<field>.`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
217
256
|
|
|
218
|
-
export { migratePublicationLists };
|
|
257
|
+
export { PUBLICATIONS_TITLE_CSS, migratePublicationLists, migrateSectionComponents };
|
|
@@ -197,4 +197,4 @@ function documentDirective(source) {
|
|
|
197
197
|
return void 0;
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
-
export {
|
|
200
|
+
export { componentName, documentDirective, findPdfPlacements, findPdfPublicationPlacements, findWebPlacements, pageRoute, publicationPlacementProblems };
|
package/dist/chunks/plan.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { readFile, mkdir, writeFile, rename } from 'node:fs/promises';
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
|
-
import { C as CommandError } from '../shared/vitakit.
|
|
4
|
+
import { C as CommandError } from '../shared/vitakit.CvYy_Quj.mjs';
|
|
5
5
|
import '../shared/vitakit.DQ6PmGoL.mjs';
|
|
6
6
|
import 'node:url';
|
|
7
7
|
import 'node:process';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { entryComponentProps, listComponentProps } from './snippets.mjs';
|
|
2
|
+
import '../shared/vitakit.DcQOdM4I.mjs';
|
|
3
|
+
|
|
4
|
+
const UNTYPED_ENTRY = "const props = defineProps<{ id?: string, entries?: Entry[], title?: string, where?: Where, limit?: number }>()";
|
|
5
|
+
const UNTYPED_LIST = "const props = defineProps<{ id: string, title?: string, where?: Where, limit?: number }>()";
|
|
6
|
+
const IDS = String.raw`'[a-z0-9-]+'(?:\s*\|\s*'[a-z0-9-]+')*`;
|
|
7
|
+
const TYPED_ENTRY = new RegExp(String.raw`^const props = defineProps<\{ id: (${IDS}), entries\?: SectionEntries<(${IDS})>, title\?: string, where\?: Where, limit\?: number \}>\(\)$`, "m");
|
|
8
|
+
const TYPED_LIST = new RegExp(String.raw`^const props = defineProps<\{ id: (${IDS}), title\?: string, where\?: Where, limit\?: number \}>\(\)$`, "m");
|
|
9
|
+
const parseIds = (type) => [...type.matchAll(/'([a-z0-9-]+)'/g)].map((match) => match[1]);
|
|
10
|
+
function sectionProps(source) {
|
|
11
|
+
const lines = source.split("\n");
|
|
12
|
+
if (lines.includes(UNTYPED_ENTRY)) return { ok: true, value: { kind: "entry", ids: [], untyped: true } };
|
|
13
|
+
if (lines.includes(UNTYPED_LIST)) return { ok: true, value: { kind: "list", ids: [], untyped: true } };
|
|
14
|
+
const entry = TYPED_ENTRY.exec(source);
|
|
15
|
+
if (entry && entry[1] === entry[2]) return { ok: true, value: { kind: "entry", ids: parseIds(entry[1]), untyped: false } };
|
|
16
|
+
const list = TYPED_LIST.exec(source);
|
|
17
|
+
if (list) return { ok: true, value: { kind: "list", ids: parseIds(list[1]), untyped: false } };
|
|
18
|
+
return { ok: false, reason: "its defineProps is not the one vitakit writes" };
|
|
19
|
+
}
|
|
20
|
+
function typeSectionProps(source, ids) {
|
|
21
|
+
const found = sectionProps(source);
|
|
22
|
+
if (!found.ok) return found;
|
|
23
|
+
const { kind, ids: current, untyped } = found.value;
|
|
24
|
+
const wanted = [.../* @__PURE__ */ new Set([...current, ...ids])];
|
|
25
|
+
if (!untyped && wanted.length === current.length) return { ok: true, value: { source, changed: false, from: found.value } };
|
|
26
|
+
const line = kind === "entry" ? entryComponentProps(wanted) : listComponentProps(wanted);
|
|
27
|
+
let result = untyped ? source.replace(kind === "entry" ? UNTYPED_ENTRY : UNTYPED_LIST, line) : source.replace(kind === "entry" ? TYPED_ENTRY : TYPED_LIST, line);
|
|
28
|
+
if (untyped) result = retypeImports(result, kind);
|
|
29
|
+
return { ok: true, value: { source: result, changed: result !== source, from: found.value } };
|
|
30
|
+
}
|
|
31
|
+
function retypeImports(source, kind) {
|
|
32
|
+
let result = source;
|
|
33
|
+
if (kind === "list") result = result.replace("const entries = computed<Entry[]>(() => {", "const entries = computed(() => {");
|
|
34
|
+
const stillUsed = /\bEntry\b/.test(result.replace(/import[^\n]*from '@anja\.codes\/vitakit\/shared'/g, ""));
|
|
35
|
+
return result.replace(/^(import (?:type )?\{)([^}\n]*)(\} from '@anja\.codes\/vitakit\/shared')$/m, (whole, start, names, end) => {
|
|
36
|
+
const typeOnly = start.startsWith("import type");
|
|
37
|
+
const list = names.split(",").map((name) => name.trim()).filter(Boolean);
|
|
38
|
+
const entry = list.findIndex((name) => name.replace(/^type /, "") === "Entry");
|
|
39
|
+
const wanted = kind === "entry" ? typeOnly ? "SectionEntries" : "type SectionEntries" : void 0;
|
|
40
|
+
if (entry !== -1 && !stillUsed) list.splice(entry, 1);
|
|
41
|
+
if (wanted && !list.some((name) => name.replace(/^type /, "") === "SectionEntries")) {
|
|
42
|
+
const at = list.findIndex((name) => name.replace(/^type /, "").localeCompare("SectionEntries") > 0);
|
|
43
|
+
list.splice(at === -1 ? list.length : at, 0, wanted);
|
|
44
|
+
}
|
|
45
|
+
if (!list.length) return "";
|
|
46
|
+
return `${start} ${list.join(", ")} ${end}`;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { sectionProps, typeSectionProps };
|
package/dist/chunks/snippets.mjs
CHANGED
|
@@ -6,10 +6,10 @@ function fieldLabel(name) {
|
|
|
6
6
|
return words.charAt(0).toUpperCase() + words.slice(1);
|
|
7
7
|
}
|
|
8
8
|
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
9
|
-
const access = (object, name) => IDENTIFIER.test(name) ? `${
|
|
9
|
+
const access = (object, path) => path.split(".").reduce((result, name) => IDENTIFIER.test(name) ? `${result}.${name}` : `${result}['${name}']`, object);
|
|
10
10
|
function webFieldSnippet(field, className = "vita-entry__note", entry = "entry") {
|
|
11
11
|
const value = access(entry, field.path);
|
|
12
|
-
const label = fieldLabel(field.path);
|
|
12
|
+
const label = fieldLabel(field.path.split(".").pop());
|
|
13
13
|
const line = (condition, content) => `<p v-if="${condition}" class="${className}">${content}</p>`;
|
|
14
14
|
if (field.type === "link") {
|
|
15
15
|
return {
|
|
@@ -37,8 +37,15 @@ function webFieldSnippet(field, className = "vita-entry__note", entry = "entry")
|
|
|
37
37
|
return { markup: line(`typeof ${value} === 'string'`, `<a :href="${value}">${label}</a>`), imports: [] };
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
if (field.type === "group" && !field.several && field.fields?.length) {
|
|
41
|
+
const parts = field.fields.map((part) => webFieldSnippet(part, className, entry));
|
|
42
|
+
return {
|
|
43
|
+
markup: [`<template v-if="${value}">`, ...parts.map((part) => ` ${part.markup}`), "</template>"].join("\n"),
|
|
44
|
+
imports: parts.flatMap((part) => part.imports)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
40
47
|
return {
|
|
41
|
-
markup: `<!-- ${field.path}: ${field.type === "group" ? "a
|
|
48
|
+
markup: `<!-- ${field.path}: ${field.type === "group" ? "a list of groups of fields" : "a value of its own kind"}; its type is in schemas/, e.g. {{ ${value} }} -->`,
|
|
42
49
|
imports: []
|
|
43
50
|
};
|
|
44
51
|
}
|
|
@@ -74,9 +81,19 @@ function sharedImport(imports) {
|
|
|
74
81
|
return names.every((name) => name.startsWith("type ")) ? [`import type { ${names.map((name) => name.slice(5)).join(", ")} } from '@anja.codes/vitakit/shared'`] : [`import { ${names.join(", ")} } from '@anja.codes/vitakit/shared'`];
|
|
75
82
|
}
|
|
76
83
|
const indent = (lines, by) => lines.map((line) => line ? by + line : line);
|
|
84
|
+
function sectionIdType(ids) {
|
|
85
|
+
return [...new Set(ids)].map((id) => `'${id}'`).join(" | ");
|
|
86
|
+
}
|
|
87
|
+
function entryComponentProps(ids) {
|
|
88
|
+
const type = sectionIdType(ids);
|
|
89
|
+
return `const props = defineProps<{ id: ${type}, entries?: SectionEntries<${type}>, title?: string, where?: Where, limit?: number }>()`;
|
|
90
|
+
}
|
|
91
|
+
function listComponentProps(ids) {
|
|
92
|
+
return `const props = defineProps<{ id: ${sectionIdType(ids)}, title?: string, where?: Where, limit?: number }>()`;
|
|
93
|
+
}
|
|
77
94
|
function entryComponent(id, name, fields, region) {
|
|
78
95
|
const snippets = fields.map((field) => webFieldSnippet(field, region === "note" ? "vita-entry__note" : "vita-entry__meta"));
|
|
79
|
-
const imports = sharedImport(["type
|
|
96
|
+
const imports = sharedImport(["type SectionEntries", "type Where", ...snippets.flatMap((snippet) => snippet.imports)]);
|
|
80
97
|
const keep = region === "note" ? ['<p v-if="display.note !== undefined" class="vita-entry__note">{{ display.note }}</p>'] : [
|
|
81
98
|
'<p v-if="display.meta.length" class="vita-entry__meta">',
|
|
82
99
|
' <template v-for="(part, index) in display.meta" :key="index">',
|
|
@@ -89,7 +106,7 @@ function entryComponent(id, name, fields, region) {
|
|
|
89
106
|
" </template>",
|
|
90
107
|
"</p>"
|
|
91
108
|
];
|
|
92
|
-
const body = fields.length ? [...keep, ...snippets.
|
|
109
|
+
const body = fields.length ? [...keep, ...snippets.flatMap((snippet) => snippet.markup.split("\n"))] : [...keep, `<!-- Add your own lines here, e.g. -->`, `<!-- <p v-if="typeof entry.audience === 'string'" class="vita-entry__${region}">Audience: {{ entry.audience }}</p> -->`];
|
|
93
110
|
return [
|
|
94
111
|
'<script setup lang="ts">',
|
|
95
112
|
"/**",
|
|
@@ -100,11 +117,12 @@ function entryComponent(id, name, fields, region) {
|
|
|
100
117
|
` * The #${region} slot below replaces that region of each entry. Delete it to get`,
|
|
101
118
|
" * vitakit's version back; the other slots are #title, #meta, #note and the",
|
|
102
119
|
" * default slot (the whole entry). `entry` has every field of the entry,",
|
|
103
|
-
|
|
120
|
+
` * typed by its schema; \`display\` what vitakit would show. \`entries\` takes`,
|
|
121
|
+
` * ${id} entries a page selected itself, e.g. filtered in its script.`,
|
|
104
122
|
" */",
|
|
105
123
|
...imports,
|
|
106
124
|
"",
|
|
107
|
-
|
|
125
|
+
entryComponentProps([id]),
|
|
108
126
|
"<\/script>",
|
|
109
127
|
"",
|
|
110
128
|
"<template>",
|
|
@@ -124,8 +142,8 @@ function listComponent(id, name, fields, groupBy) {
|
|
|
124
142
|
const titleLine = titleField ? titleField.type === "formatted-text" ? `<p class="vita-entry__title" v-html="richHtml(${access("entry", titleField.path)})" />` : `<p class="vita-entry__title">{{ ${access("entry", titleField.path)} }}</p>` : '<!-- <p class="vita-entry__title">{{ entry.title }}</p> -->';
|
|
125
143
|
const imports = sharedImport([
|
|
126
144
|
"filterWhere",
|
|
127
|
-
"type Entry",
|
|
128
145
|
"type Where",
|
|
146
|
+
...groupBy ? ["type SectionEntries"] : [],
|
|
129
147
|
...fields.some((field) => field.path === "date") ? ["type DateRange"] : [],
|
|
130
148
|
...titleField?.type === "formatted-text" ? ["richHtml"] : [],
|
|
131
149
|
...snippets.flatMap((snippet) => snippet.imports)
|
|
@@ -133,7 +151,7 @@ function listComponent(id, name, fields, groupBy) {
|
|
|
133
151
|
const hasDate = fields.some((field) => field.path === "date");
|
|
134
152
|
const row = [
|
|
135
153
|
`<VitaEntry v-for="entry in ${groupBy ? "group.entries" : "entries"}" :key="entry.id"${hasDate ? ' :date="entry.date as DateRange"' : ""}>`,
|
|
136
|
-
...indent([titleLine, ...snippets.
|
|
154
|
+
...indent([titleLine, ...snippets.flatMap((snippet) => snippet.markup.split("\n"))], " "),
|
|
137
155
|
"</VitaEntry>"
|
|
138
156
|
];
|
|
139
157
|
const grouping = groupBy ? [
|
|
@@ -141,7 +159,7 @@ function listComponent(id, name, fields, groupBy) {
|
|
|
141
159
|
`// Entries in a row with the same ${groupBy} form a group, in the order`,
|
|
142
160
|
"// vitakit sorted them (newest first).",
|
|
143
161
|
"const groups = computed(() => {",
|
|
144
|
-
" const result: { label: string, entries:
|
|
162
|
+
" const result: { label: string, entries: SectionEntries<typeof props.id> }[] = []",
|
|
145
163
|
" for (const entry of entries.value) {",
|
|
146
164
|
` const label = String(${access("entry", groupBy)} ?? '')`,
|
|
147
165
|
" const last = result[result.length - 1]",
|
|
@@ -175,9 +193,9 @@ function listComponent(id, name, fields, groupBy) {
|
|
|
175
193
|
`import { computed } from 'vue'`,
|
|
176
194
|
...imports,
|
|
177
195
|
"",
|
|
178
|
-
|
|
196
|
+
listComponentProps([id]),
|
|
179
197
|
"",
|
|
180
|
-
"const entries = computed
|
|
198
|
+
"const entries = computed(() => {",
|
|
181
199
|
" const selected = filterWhere(useSection(props.id), props.where)",
|
|
182
200
|
" return props.limit ? selected.slice(0, props.limit) : selected",
|
|
183
201
|
"})",
|
|
@@ -196,4 +214,4 @@ function listComponent(id, name, fields, groupBy) {
|
|
|
196
214
|
].join("\n");
|
|
197
215
|
}
|
|
198
216
|
|
|
199
|
-
export { entryComponent, etaFieldSnippet, fieldLabel, listComponent, webFieldSnippet };
|
|
217
|
+
export { entryComponent, entryComponentProps, etaFieldSnippet, fieldLabel, listComponent, listComponentProps, sectionIdType, webFieldSnippet };
|
package/dist/chunks/validate.mjs
CHANGED
|
@@ -32,7 +32,7 @@ import '../shared/vitakit.DvABWuUc.mjs';
|
|
|
32
32
|
import '../shared/vitakit.TSMTC5Jl.mjs';
|
|
33
33
|
import './fields.mjs';
|
|
34
34
|
import './documents.mjs';
|
|
35
|
-
import '
|
|
35
|
+
import './placements.mjs';
|
|
36
36
|
import '../shared/vitakit.D-DdJaDZ.mjs';
|
|
37
37
|
import 'eta';
|
|
38
38
|
import '../shared/vitakit.DcQOdM4I.mjs';
|
package/dist/cli.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { a as aligned, c as commands, d as detectPackageManager, m as main } from './shared/vitakit.
|
|
1
|
+
export { a as aligned, c as commands, d as detectPackageManager, m as main } from './shared/vitakit.CvYy_Quj.mjs';
|
|
2
2
|
import 'node:fs';
|
|
3
3
|
import 'node:path';
|
|
4
4
|
import './shared/vitakit.DQ6PmGoL.mjs';
|
package/dist/index.d.mts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import * as zod from 'zod';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
export { z } from 'zod';
|
|
4
|
-
import { D as DateRange, V as Visibility, a as VitakitContent } from './shared/vitakit.
|
|
4
|
+
import { D as DateRange, V as Visibility, a as VitakitContent } from './shared/vitakit.CB8b91Lh.mjs';
|
|
5
5
|
import { RichTextMarker } from './shared/index.mjs';
|
|
6
|
-
export { C as ContentIssue, b as DatePoint, E as Entry, c as EntryBase, L as Link, d as Linked, P as Page, e as Profile, f as Publication, g as PublicationBadge, h as PublicationGroup, i as PublicationList, j as RichText } from './shared/vitakit.
|
|
6
|
+
export { C as ContentIssue, b as DatePoint, E as Entry, c as EntryBase, L as Link, d as Linked, P as Page, e as Profile, f as Publication, g as PublicationBadge, h as PublicationGroup, i as PublicationList, j as RichText } from './shared/vitakit.CB8b91Lh.mjs';
|
|
7
7
|
import { BadgeConfig } from '@behackl/citation-js-extras';
|
|
8
8
|
export { BadgeConfig, badgePresets } from '@behackl/citation-js-extras';
|
|
9
9
|
import * as zod_v4_core from 'zod/v4/core';
|
|
@@ -675,11 +675,7 @@ type BuiltinSection = keyof typeof schemas;
|
|
|
675
675
|
* export default extendSection(schemas.teaching, { evaluationScore: z.number().optional() })
|
|
676
676
|
* ```
|
|
677
677
|
*/
|
|
678
|
-
declare function extendSection<Shape extends z.ZodRawShape>(base: z.ZodArray<
|
|
679
|
-
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
680
|
-
}> & { -readonly [P in keyof Shape]: Shape[P]; } : (Readonly<{
|
|
681
|
-
[k: string]: z.core.$ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
682
|
-
}> extends infer T_1 extends z.core.util.SomeObject ? { [K in keyof T_1 as K extends keyof Shape ? never : K]: T_1[K]; } : never) & { [K_1 in keyof { -readonly [P in keyof Shape]: Shape[P]; }]: { -readonly [P in keyof Shape]: Shape[P]; }[K_1]; }) extends infer T ? { [k in keyof T]: T[k]; } : never, z.core.$strip>>;
|
|
678
|
+
declare function extendSection<Base extends z.ZodObject, Shape extends z.ZodRawShape>(base: z.ZodArray<Base>, shape: Shape): z.ZodArray<z.ZodObject<z.util.Extend<Base['shape'], Shape>, Base['_zod']['config']>>;
|
|
683
679
|
|
|
684
680
|
interface LoadContentOptions {
|
|
685
681
|
/** Throw on the first problem (builds) or collect them (dev). */
|
package/dist/nuxt.mjs
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
import { defineNuxtModule, createResolver, addTemplate, addTypeTemplate, addImportsDir, addComponentsDir, addPrerenderRoutes, updateTemplates } from '@nuxt/kit';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
3
|
import { l as loadContent } from './shared/vitakit.DteA9vLy.mjs';
|
|
4
|
+
import { existsSync } from 'node:fs';
|
|
5
|
+
import { join, relative, sep, isAbsolute, resolve } from 'node:path';
|
|
6
|
+
import { schemas } from './chunks/schemas.mjs';
|
|
7
|
+
import { findSectionFiles } from './chunks/sections.mjs';
|
|
4
8
|
import { f as fail } from './shared/vitakit.DCK90zq4.mjs';
|
|
5
|
-
import {
|
|
9
|
+
import { findWebPlacements, publicationPlacementProblems } from './chunks/placements.mjs';
|
|
6
10
|
import { readdir } from 'node:fs/promises';
|
|
7
|
-
import { isAbsolute, resolve, relative } from 'node:path';
|
|
8
11
|
import 'gray-matter';
|
|
9
12
|
import 'yaml';
|
|
10
13
|
import './chunks/bibliography.mjs';
|
|
11
|
-
import 'node:fs';
|
|
12
14
|
import './shared/vitakit.DQ6PmGoL.mjs';
|
|
13
15
|
import 'node:url';
|
|
14
16
|
import '@behackl/citation-js-extras';
|
|
@@ -26,11 +28,35 @@ import 'unist-util-visit';
|
|
|
26
28
|
import 'zod';
|
|
27
29
|
import './chunks/publication-lists.mjs';
|
|
28
30
|
import './shared/vitakit.D10HyDhF.mjs';
|
|
29
|
-
import './chunks/sections.mjs';
|
|
30
|
-
import './chunks/schemas.mjs';
|
|
31
31
|
import './shared/vitakit.DvABWuUc.mjs';
|
|
32
32
|
import './shared/vitakit.TSMTC5Jl.mjs';
|
|
33
33
|
|
|
34
|
+
async function sectionTypeSources(rootDir) {
|
|
35
|
+
const { files } = await findSectionFiles(rootDir).catch(() => ({ files: [] }));
|
|
36
|
+
return files.map(({ id }) => {
|
|
37
|
+
const file = join(rootDir, "schemas", `${id}.ts`);
|
|
38
|
+
return { id, ...existsSync(file) ? { file } : {}, builtIn: Object.hasOwn(schemas, id) };
|
|
39
|
+
}).filter((source) => source.file !== void 0 || source.builtIn);
|
|
40
|
+
}
|
|
41
|
+
function sectionTypesDeclaration(sources, buildDir) {
|
|
42
|
+
const dir = join(buildDir, "vitakit");
|
|
43
|
+
const lines = sources.map(({ id, file }) => {
|
|
44
|
+
const schema = file !== void 0 ? `typeof import('${relative(dir, file).split(sep).join("/").replace(/\.ts$/, "")}').default` : `typeof import('@anja.codes/vitakit').schemas[${JSON.stringify(id)}]`;
|
|
45
|
+
return ` ${JSON.stringify(id)}: SectionEntry<${schema}>`;
|
|
46
|
+
});
|
|
47
|
+
return [
|
|
48
|
+
"// Generated by vitakit from content/*.yml and schemas/; do not edit.",
|
|
49
|
+
`import type { SectionEntry } from '@anja.codes/vitakit/shared'`,
|
|
50
|
+
`declare module '@anja.codes/vitakit/shared' {`,
|
|
51
|
+
` interface VitakitSections {`,
|
|
52
|
+
...lines,
|
|
53
|
+
` }`,
|
|
54
|
+
`}`,
|
|
55
|
+
`export {}`,
|
|
56
|
+
""
|
|
57
|
+
].join("\n");
|
|
58
|
+
}
|
|
59
|
+
|
|
34
60
|
function isWithin(directory, path) {
|
|
35
61
|
const child = relative(directory, path);
|
|
36
62
|
return child === "" || !isAbsolute(child) && child.split(/[/\\]/)[0] !== "..";
|
|
@@ -60,6 +86,7 @@ function katexStylesheet() {
|
|
|
60
86
|
}
|
|
61
87
|
const TEMPLATE = "vitakit/content.mjs";
|
|
62
88
|
const TYPES = "vitakit/content.d.ts";
|
|
89
|
+
const SECTION_TYPES = "vitakit/sections.d.ts";
|
|
63
90
|
function report(content) {
|
|
64
91
|
if (!content.issues.length) return;
|
|
65
92
|
const lines = content.issues.map((issue) => ` ${issue.where}: ${issue.message.replace(/\n/g, "\n ")}`);
|
|
@@ -109,6 +136,11 @@ const module$1 = defineNuxtModule({
|
|
|
109
136
|
""
|
|
110
137
|
].join("\n")
|
|
111
138
|
});
|
|
139
|
+
let sections = await sectionTypeSources(rootDir);
|
|
140
|
+
addTypeTemplate({
|
|
141
|
+
filename: SECTION_TYPES,
|
|
142
|
+
getContents: () => sectionTypesDeclaration(sections, nuxt.options.buildDir)
|
|
143
|
+
});
|
|
112
144
|
if (options.katexCss !== false) nuxt.options.css.push(katexStylesheet());
|
|
113
145
|
nuxt.options.build.transpile.push(resolver.resolve("./runtime"));
|
|
114
146
|
addImportsDir(resolver.resolve("./runtime/composables"));
|
|
@@ -117,8 +149,15 @@ const module$1 = defineNuxtModule({
|
|
|
117
149
|
if (!nuxt.options.dev) return;
|
|
118
150
|
await watchComponentOverrides(nuxt, resolver.resolve("./runtime/components"));
|
|
119
151
|
nuxt.options.watch.push("../content", "../schemas");
|
|
120
|
-
nuxt.hook("builder:watch", async (
|
|
152
|
+
nuxt.hook("builder:watch", async (event, path) => {
|
|
121
153
|
if (!/[/\\](content|schemas)[/\\]/.test(path)) return;
|
|
154
|
+
if (event === "add" || event === "unlink") {
|
|
155
|
+
const found = await sectionTypeSources(rootDir);
|
|
156
|
+
if (JSON.stringify(found) !== JSON.stringify(sections)) {
|
|
157
|
+
sections = found;
|
|
158
|
+
await updateTemplates({ filter: (item) => item.filename === SECTION_TYPES });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
122
161
|
try {
|
|
123
162
|
content = await load(false);
|
|
124
163
|
} catch (error) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
1
|
+
<script setup lang="ts" generic="Id extends string = string">
|
|
2
2
|
/**
|
|
3
3
|
* A generic section with conventional field selection and optional overrides.
|
|
4
4
|
*
|
|
@@ -18,14 +18,15 @@ import {
|
|
|
18
18
|
richHtml,
|
|
19
19
|
type DateRange,
|
|
20
20
|
type Entry,
|
|
21
|
+
type SectionEntries,
|
|
21
22
|
type VitaSectionSlotProps,
|
|
22
23
|
type Where,
|
|
23
24
|
} from '@anja.codes/vitakit/shared'
|
|
24
25
|
|
|
25
26
|
const props = defineProps<{
|
|
26
27
|
/** Section id; ignored when `entries` is given. */
|
|
27
|
-
id?:
|
|
28
|
-
entries?:
|
|
28
|
+
id?: Id
|
|
29
|
+
entries?: SectionEntries<Id>
|
|
29
30
|
/** Fields to try, in order, for the main line. */
|
|
30
31
|
titleFields?: string[]
|
|
31
32
|
/** Only entries whose fields have these values, as `where` in a PDF document. */
|
|
@@ -35,23 +36,24 @@ const props = defineProps<{
|
|
|
35
36
|
title?: string
|
|
36
37
|
}>()
|
|
37
38
|
|
|
39
|
+
type Row = VitaSectionSlotProps<SectionEntries<Id>[number]>
|
|
38
40
|
const slots = defineSlots<{
|
|
39
|
-
title?(props:
|
|
40
|
-
meta?(props:
|
|
41
|
-
note?(props:
|
|
42
|
-
default?(props:
|
|
41
|
+
title?(props: Row): VNodeChild
|
|
42
|
+
meta?(props: Row): VNodeChild
|
|
43
|
+
note?(props: Row): VNodeChild
|
|
44
|
+
default?(props: Row): VNodeChild
|
|
43
45
|
}>()
|
|
44
46
|
|
|
45
47
|
const rows = computed(() => {
|
|
46
|
-
const source = filterWhere(props.entries ?? (props.id ? useSection(props.id) : []), props.where)
|
|
48
|
+
const source = filterWhere((props.entries ?? (props.id ? useSection(props.id) : [])) as Entry[], props.where)
|
|
47
49
|
const items = props.limit ? source.slice(0, props.limit) : source
|
|
48
|
-
return items.map((entry, index):
|
|
50
|
+
return items.map((entry, index): Row => {
|
|
49
51
|
// Field selection remains shared with the emitter; only the web-specific
|
|
50
52
|
// presentation (such as a term replacing the date) is decided here.
|
|
51
53
|
const titleField = entryTitleField(entry, props.titleFields)
|
|
52
54
|
const term = typeof entry.term === 'string' && entry.term.length > 0 ? entry.term : undefined
|
|
53
55
|
return {
|
|
54
|
-
entry,
|
|
56
|
+
entry: entry as Row['entry'],
|
|
55
57
|
index,
|
|
56
58
|
display: {
|
|
57
59
|
titleHtml: (titleField && richHtml(entry[titleField])) || '',
|
|
@@ -74,7 +76,7 @@ const rows = computed(() => {
|
|
|
74
76
|
<h2 class="vita-section-group__title">
|
|
75
77
|
{{ title }}
|
|
76
78
|
</h2>
|
|
77
|
-
<VitaSection :entries="rows.map(row => row.entry)" :title-fields="titleFields">
|
|
79
|
+
<VitaSection :entries="rows.map(row => row.entry as Entry)" :title-fields="titleFields">
|
|
78
80
|
<template v-for="(_, name) in slots" #[name]="scope">
|
|
79
81
|
<slot :name="name" v-bind="scope" />
|
|
80
82
|
</template>
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* chunk.
|
|
7
7
|
*/
|
|
8
8
|
import content from '#vitakit/content'
|
|
9
|
-
import type {
|
|
9
|
+
import type { Page, Profile, PublicationList, SectionEntries, VitakitContent } from '@anja.codes/vitakit/shared'
|
|
10
10
|
|
|
11
11
|
export function useVitakit(): VitakitContent {
|
|
12
12
|
return content
|
|
@@ -23,8 +23,8 @@ export function useProfile(): Profile {
|
|
|
23
23
|
* Unknown ids return an empty list rather than throwing, so deleting a content
|
|
24
24
|
* file cannot break a page that still mentions it.
|
|
25
25
|
*/
|
|
26
|
-
export function useSection(id:
|
|
27
|
-
return content.sections[id] ?? []
|
|
26
|
+
export function useSection<Id extends string>(id: Id): SectionEntries<Id> {
|
|
27
|
+
return (content.sections[id] ?? []) as SectionEntries<Id>
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** All section ids that have content. */
|
package/dist/shared/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as DateRange, d as Linked, L as Link, E as Entry } from './vitakit.
|
|
2
|
-
export { C as ContentIssue, b as DatePoint, c as EntryBase, P as Page, e as Profile, f as Publication, g as PublicationBadge, h as PublicationGroup, i as PublicationList, j as RichText, R as RichTextMarker, V as Visibility, a as VitakitContent } from './vitakit.
|
|
1
|
+
import { D as DateRange, d as Linked, L as Link, E as Entry } from './vitakit.CB8b91Lh.mjs';
|
|
2
|
+
export { C as ContentIssue, b as DatePoint, c as EntryBase, P as Page, e as Profile, f as Publication, g as PublicationBadge, h as PublicationGroup, i as PublicationList, k as Rendered, j as RichText, R as RichTextMarker, S as SectionEntries, l as SectionEntry, V as Visibility, a as VitakitContent, m as VitakitSections } from './vitakit.CB8b91Lh.mjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Presentation helpers shared by the website and the LaTeX side.
|
|
@@ -69,8 +69,8 @@ interface VitaSectionDisplay {
|
|
|
69
69
|
ongoing: boolean;
|
|
70
70
|
}
|
|
71
71
|
/** Shared context for VitaSection's title, meta, note and default (body) slots. */
|
|
72
|
-
interface VitaSectionSlotProps {
|
|
73
|
-
entry:
|
|
72
|
+
interface VitaSectionSlotProps<E = Entry> {
|
|
73
|
+
entry: E;
|
|
74
74
|
/** Zero-based position in the displayed list, after limiting. */
|
|
75
75
|
index: number;
|
|
76
76
|
display: VitaSectionDisplay;
|
|
@@ -151,6 +151,15 @@ interface ContentIssue {
|
|
|
151
151
|
where: string;
|
|
152
152
|
message: string;
|
|
153
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Section id -> its entries' type. Empty here; a project's
|
|
156
|
+
* `.nuxt/vitakit/sections.d.ts` adds one key per `content/<id>.yml`, from its
|
|
157
|
+
* schema, so that `useSection('talks')` is typed by `schemas/talks.ts`.
|
|
158
|
+
*/
|
|
159
|
+
interface VitakitSections {
|
|
160
|
+
}
|
|
161
|
+
/** The entries of a section: typed by its schema when the section is known. */
|
|
162
|
+
type SectionEntries<Id extends string> = Id extends keyof VitakitSections ? VitakitSections[Id][] : Entry[];
|
|
154
163
|
/** Everything compiled from `content/`, as the app sees it. */
|
|
155
164
|
interface VitakitContent {
|
|
156
165
|
profile: Profile;
|
|
@@ -162,5 +171,32 @@ interface VitakitContent {
|
|
|
162
171
|
/** Always empty in a production build, which fails instead. */
|
|
163
172
|
issues: ContentIssue[];
|
|
164
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* A path on the site before it is resolved: `{ __vitakitPath }`. Internal, like
|
|
176
|
+
* `RichTextMarker`; components receive a string.
|
|
177
|
+
*/
|
|
178
|
+
interface SitePathMarkerShape {
|
|
179
|
+
__vitakitPath: string;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* A value as a component receives it: rendered text instead of its marker,
|
|
183
|
+
* resolved paths instead of theirs, at any depth.
|
|
184
|
+
*/
|
|
185
|
+
type Rendered<T> = T extends RichTextMarker ? RichText : T extends SitePathMarkerShape ? string : T extends DateRange ? DateRange : T extends readonly (infer Item)[] ? Rendered<Item>[] : T extends object ? {
|
|
186
|
+
[Key in keyof T]: Rendered<T[Key]>;
|
|
187
|
+
} : T;
|
|
188
|
+
/**
|
|
189
|
+
* One entry of a section, as `useSection` and the slots of `VitaSection` see
|
|
190
|
+
* it, from the section's schema: `SectionEntry<typeof schema>`.
|
|
191
|
+
*/
|
|
192
|
+
type SectionEntry<Schema extends {
|
|
193
|
+
_zod: {
|
|
194
|
+
output: readonly unknown[];
|
|
195
|
+
};
|
|
196
|
+
}> = Flat<Rendered<Schema['_zod']['output'][number]> & EntryBase>;
|
|
197
|
+
/** One object type instead of an intersection: readable on hover, and assignable to `Entry`. */
|
|
198
|
+
type Flat<T> = {
|
|
199
|
+
[Key in keyof T]: T[Key];
|
|
200
|
+
};
|
|
165
201
|
|
|
166
|
-
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, PublicationList as i, RichText as j };
|
|
202
|
+
export type { ContentIssue as C, DateRange as D, Entry as E, Link as L, Page as P, RichTextMarker as R, SectionEntries as S, 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, PublicationList as i, RichText as j, Rendered as k, SectionEntry as l, VitakitSections as m };
|
|
@@ -781,6 +781,11 @@ async function planWeb(context, ws, inv, section, fields, list, values, notes) {
|
|
|
781
781
|
if (!inv.web.components.has(component)) {
|
|
782
782
|
throw new CommandError(`--use ${component}: no app/components/${component}.vue; the components are ${[...inv.web.components.keys()].join(", ") || "(none)"}`);
|
|
783
783
|
}
|
|
784
|
+
const { typeSectionProps } = await import('../chunks/props.mjs');
|
|
785
|
+
const file = inv.web.components.get(component);
|
|
786
|
+
const typed = typeSectionProps(await ws.read(file), [id]);
|
|
787
|
+
if (!typed.ok) notes.after.push(`By hand: ${file} ${typed.reason}; for ${id}'s entries to be typed in it, its id should accept '${id}' too, e.g. id: 'talks' | '${id}'.`);
|
|
788
|
+
else if (typed.value.changed) await ws.edit(file, typed.value.source, `shows ${id} too: its id accepts '${id}'`);
|
|
784
789
|
} else {
|
|
785
790
|
const fallback = `${pascal(id)}${list ? "List" : "Section"}`;
|
|
786
791
|
component = values.component ?? (interactive ? await prompt.input(io, "Name of the component", fallback) : fallback);
|
|
@@ -1192,9 +1197,9 @@ async function fieldSnippets(context, inv, section, names) {
|
|
|
1192
1197
|
' <template #note="{ entry, display }">',
|
|
1193
1198
|
' <p v-if="display.note !== undefined" class="vita-entry__note">{{ display.note }}</p>'
|
|
1194
1199
|
);
|
|
1195
|
-
lines.push(...web.
|
|
1200
|
+
lines.push(...web.flatMap((snippet) => snippet.markup.split("\n")).map((line) => ` ${line}`), " </template>", " </VitaSection>");
|
|
1196
1201
|
}
|
|
1197
|
-
if (components.length) lines.push("", ...indented(web.
|
|
1202
|
+
if (components.length) lines.push("", ...indented(web.flatMap((snippet) => snippet.markup.split("\n"))));
|
|
1198
1203
|
if (imports.length) lines.push("", ` import { ${imports.join(", ")} } from '@anja.codes/vitakit/shared'`);
|
|
1199
1204
|
lines.push("");
|
|
1200
1205
|
if (existsSync2(join2(root, "cv-tex", "templates", `${id}.eta`))) {
|
|
@@ -1492,10 +1497,11 @@ nothing that is up to date.
|
|
|
1492
1497
|
Then check the project: \`${run.bin("vitakit", "validate")}\``,
|
|
1493
1498
|
async run(context, { values }) {
|
|
1494
1499
|
const { Workspace } = await import('../chunks/plan.mjs');
|
|
1495
|
-
const { migratePublicationLists } = await import('../chunks/migrations.mjs');
|
|
1500
|
+
const { migratePublicationLists, migrateSectionComponents } = await import('../chunks/migrations.mjs');
|
|
1496
1501
|
const ws = new Workspace(context.root);
|
|
1497
1502
|
const notes = { before: [], after: [] };
|
|
1498
1503
|
await migratePublicationLists(context.root, ws, notes);
|
|
1504
|
+
await migrateSectionComponents(context.root, ws, notes);
|
|
1499
1505
|
notes.after.push(`Check the project: \`${context.run.bin("vitakit", "validate")}\``);
|
|
1500
1506
|
return finish(context, ws, values, notes);
|
|
1501
1507
|
}
|
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>;
|
package/dist/tex.mjs
CHANGED
|
@@ -33,7 +33,7 @@ import './chunks/sections.mjs';
|
|
|
33
33
|
import './chunks/schemas.mjs';
|
|
34
34
|
import './shared/vitakit.DvABWuUc.mjs';
|
|
35
35
|
import './shared/vitakit.TSMTC5Jl.mjs';
|
|
36
|
-
import './
|
|
36
|
+
import './chunks/placements.mjs';
|
|
37
37
|
import 'eta';
|
|
38
38
|
import './shared/vitakit.CfB3hMHl.mjs';
|
|
39
39
|
import './shared/vitakit.D-DdJaDZ.mjs';
|