@anja.codes/vitakit 0.1.0 → 0.3.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/README.md +52 -131
- package/dist/check.mjs +20 -11
- package/dist/chunks/authoring.mjs +372 -0
- package/dist/{shared/vitakit.CypCF-4Q.mjs → chunks/bibliography.mjs} +74 -237
- package/dist/chunks/build.mjs +178 -0
- package/dist/chunks/documents.mjs +152 -0
- package/dist/chunks/draft.mjs +222 -0
- package/dist/chunks/eject.mjs +59 -0
- package/dist/chunks/emit.mjs +216 -0
- package/dist/chunks/eta.mjs +16 -0
- package/dist/chunks/fields.mjs +79 -0
- package/dist/chunks/inventory.mjs +201 -0
- package/dist/chunks/migrations.mjs +218 -0
- package/dist/chunks/plan.mjs +172 -0
- package/dist/chunks/prompt.mjs +33 -0
- package/dist/chunks/publication-lists.mjs +202 -0
- package/dist/chunks/schema-source.mjs +12 -0
- package/dist/chunks/schemas.mjs +109 -0
- package/dist/chunks/sections.mjs +109 -0
- package/dist/chunks/snippets.mjs +199 -0
- package/dist/chunks/validate.mjs +133 -0
- package/dist/chunks/vue.mjs +153 -0
- package/dist/chunks/yaml.mjs +62 -0
- package/dist/cli.d.mts +33 -40
- package/dist/cli.mjs +7 -224
- package/dist/index.d.mts +379 -49
- package/dist/index.mjs +15 -7
- package/dist/nuxt.mjs +48 -10
- package/dist/runtime/components/VitaPublications.vue +21 -22
- package/dist/runtime/components/VitaSection.vue +94 -38
- package/dist/runtime/composables/useVitakit.ts +14 -3
- package/dist/schemas/education.ts +4 -4
- package/dist/schemas/employment.ts +3 -3
- package/dist/schemas/publications.ts +10 -8
- package/dist/schemas/supervision.ts +3 -3
- package/dist/schemas/talks.ts +1 -1
- package/dist/schemas/teaching.ts +3 -3
- package/dist/shared/index.d.mts +60 -9
- package/dist/shared/index.mjs +3 -57
- package/dist/shared/{vitakit.D8WRg_Xx.d.mts → vitakit.BfR_mUHm.d.mts} +25 -12
- package/dist/shared/vitakit.CCTDg7pz.mjs +199 -0
- package/dist/shared/vitakit.CfB3hMHl.mjs +60 -0
- package/dist/shared/{vitakit.BlU-p01e.mjs → vitakit.CtChKO0I.mjs} +1 -1
- package/dist/shared/vitakit.D-DdJaDZ.mjs +20 -0
- package/dist/shared/vitakit.D10HyDhF.mjs +19 -0
- package/dist/shared/vitakit.DCK90zq4.mjs +67 -0
- package/dist/shared/vitakit.DQ6PmGoL.mjs +34 -0
- package/dist/shared/vitakit.DcQOdM4I.mjs +57 -0
- package/dist/shared/vitakit.DteA9vLy.mjs +135 -0
- package/dist/shared/vitakit.DvABWuUc.mjs +121 -0
- package/dist/shared/vitakit.DvvUYUTS.mjs +200 -0
- package/dist/shared/vitakit.TSMTC5Jl.mjs +6 -0
- package/dist/shared/vitakit.UfoXdL7U.mjs +22 -0
- package/dist/shared/vitakit.zwr1ssO5.mjs +1850 -0
- package/dist/tex/templates/_generic.eta +6 -4
- package/dist/tex/templates/education.eta +10 -5
- package/dist/tex/templates/employment.eta +12 -5
- package/dist/tex/templates/publications.eta +7 -3
- package/dist/tex/templates/supervision.eta +10 -2
- package/dist/tex/templates/talks.eta +5 -1
- package/dist/tex/templates/teaching.eta +6 -4
- package/dist/tex/vitakit-cv.cls +36 -10
- package/dist/tex.d.mts +80 -11
- package/dist/tex.mjs +27 -117
- package/package.json +10 -7
- package/src/bin/vitakit.mjs +3 -153
- package/dist/shared/vitakit.BRhjqpAt.mjs +0 -250
- package/dist/shared/vitakit.BxcnlFGP.mjs +0 -325
- package/src/bin/vitakit-build-cv.mjs +0 -50
- package/src/bin/vitakit-check.mjs +0 -57
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { join, sep, isAbsolute, relative } from 'node:path';
|
|
2
|
+
import { parseExpression } from '@babel/parser';
|
|
3
|
+
import { l as loadContent } from '../shared/vitakit.DteA9vLy.mjs';
|
|
4
|
+
import { inventory } from './inventory.mjs';
|
|
5
|
+
import { m as messageOf, w as whereOf } from '../shared/vitakit.DCK90zq4.mjs';
|
|
6
|
+
import { assertTemplatesAreUsed, renderSection, renderPublications } from './emit.mjs';
|
|
7
|
+
import 'node:fs/promises';
|
|
8
|
+
import 'gray-matter';
|
|
9
|
+
import 'yaml';
|
|
10
|
+
import './bibliography.mjs';
|
|
11
|
+
import 'node:fs';
|
|
12
|
+
import '../shared/vitakit.DQ6PmGoL.mjs';
|
|
13
|
+
import 'node:url';
|
|
14
|
+
import '@behackl/citation-js-extras';
|
|
15
|
+
import 'unified';
|
|
16
|
+
import 'rehype-parse';
|
|
17
|
+
import 'rehype-sanitize';
|
|
18
|
+
import 'rehype-stringify';
|
|
19
|
+
import 'remark-parse';
|
|
20
|
+
import 'remark-gfm';
|
|
21
|
+
import 'remark-math';
|
|
22
|
+
import 'remark-rehype';
|
|
23
|
+
import 'rehype-katex';
|
|
24
|
+
import 'rehype-raw';
|
|
25
|
+
import 'unist-util-visit';
|
|
26
|
+
import 'zod';
|
|
27
|
+
import './publication-lists.mjs';
|
|
28
|
+
import '../shared/vitakit.D10HyDhF.mjs';
|
|
29
|
+
import './sections.mjs';
|
|
30
|
+
import './schemas.mjs';
|
|
31
|
+
import '../shared/vitakit.DvABWuUc.mjs';
|
|
32
|
+
import '../shared/vitakit.TSMTC5Jl.mjs';
|
|
33
|
+
import './fields.mjs';
|
|
34
|
+
import './documents.mjs';
|
|
35
|
+
import '../shared/vitakit.DvvUYUTS.mjs';
|
|
36
|
+
import '../shared/vitakit.D-DdJaDZ.mjs';
|
|
37
|
+
import 'eta';
|
|
38
|
+
import '../shared/vitakit.DcQOdM4I.mjs';
|
|
39
|
+
import '../shared/vitakit.CfB3hMHl.mjs';
|
|
40
|
+
|
|
41
|
+
function literalKeys(expression) {
|
|
42
|
+
try {
|
|
43
|
+
const node = parseExpression(expression, { plugins: ["typescript"] });
|
|
44
|
+
if (node.type !== "ObjectExpression") return void 0;
|
|
45
|
+
const keys = [];
|
|
46
|
+
for (const property of node.properties) {
|
|
47
|
+
if (property.type !== "ObjectProperty" || property.computed) return void 0;
|
|
48
|
+
if (property.key.type === "Identifier") keys.push(property.key.name);
|
|
49
|
+
else if (property.key.type === "StringLiteral") keys.push(property.key.value);
|
|
50
|
+
else return void 0;
|
|
51
|
+
}
|
|
52
|
+
return keys;
|
|
53
|
+
} catch {
|
|
54
|
+
return void 0;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function validateProject(rootDir, { sections: only } = {}) {
|
|
58
|
+
const inv = await inventory(rootDir);
|
|
59
|
+
const problems = [];
|
|
60
|
+
const seen = /* @__PURE__ */ new Set();
|
|
61
|
+
const add = (problem) => {
|
|
62
|
+
const key = `${problem.where}
|
|
63
|
+
${problem.message}`;
|
|
64
|
+
if (seen.has(key)) return;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
problems.push(problem);
|
|
67
|
+
};
|
|
68
|
+
const local = (where) => isAbsolute(where) ? relative(rootDir, where) : where;
|
|
69
|
+
for (const problem of inv.problems) {
|
|
70
|
+
const section = inv.sections.find((candidate) => candidate.problems.includes(problem));
|
|
71
|
+
add({ ...problem, ...section ? { section: section.id } : {} });
|
|
72
|
+
}
|
|
73
|
+
const valid = new Map(inv.sections.filter((section) => !section.problems.length).map((section) => [section.id, section]));
|
|
74
|
+
const known = new Set(inv.sections.map((section) => section.id));
|
|
75
|
+
for (const placement of inv.web.placements) {
|
|
76
|
+
const where = `${placement.file}:${placement.line}`;
|
|
77
|
+
if (!known.has(placement.section)) {
|
|
78
|
+
add({ where, section: placement.section, message: `no section called "${placement.section}" (no content/${placement.section}.yml)` });
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const fields = valid.get(placement.section)?.fields;
|
|
82
|
+
if (placement.where === void 0 || !fields) continue;
|
|
83
|
+
const keys = literalKeys(placement.where);
|
|
84
|
+
if (!keys) continue;
|
|
85
|
+
const names = /* @__PURE__ */ new Set(["year", ...fields.map((field) => field.path)]);
|
|
86
|
+
for (const key of keys.filter((key2) => !names.has(key2))) {
|
|
87
|
+
add({ where, section: placement.section, message: `where.${key}: ${placement.section} has no field "${key}"; its fields are ${[...names].join(", ")}` });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
if (inv.documents.length) {
|
|
91
|
+
const templateDir = join(rootDir, "cv-tex", "templates");
|
|
92
|
+
try {
|
|
93
|
+
await assertTemplatesAreUsed(templateDir, [...known]);
|
|
94
|
+
} catch (error) {
|
|
95
|
+
add({ where: local(whereOf(error, "cv-tex/templates/")), message: messageOf(error) });
|
|
96
|
+
}
|
|
97
|
+
const content = await loadContent(rootDir, { strict: false, maxVisibility: "private", target: "pdf" });
|
|
98
|
+
for (const document of inv.documents) {
|
|
99
|
+
for (const placement of document.placements) {
|
|
100
|
+
try {
|
|
101
|
+
if (valid.has(placement.section)) {
|
|
102
|
+
await renderSection(placement.section, content.sections[placement.section] ?? [], document.options, templateDir);
|
|
103
|
+
}
|
|
104
|
+
} catch (error) {
|
|
105
|
+
add({
|
|
106
|
+
where: local(whereOf(error, `cv-tex/templates/${placement.section}.eta`)),
|
|
107
|
+
section: placement.section,
|
|
108
|
+
message: `${messageOf(error)} (in ${document.tex})`
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
for (const placement of document.publications) {
|
|
113
|
+
const list = content.publications[placement.list];
|
|
114
|
+
if (!list) continue;
|
|
115
|
+
try {
|
|
116
|
+
await renderPublications(list, templateDir, content.profile.url);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
add({ where: local(whereOf(error, "cv-tex/templates/publications.eta")), message: `${messageOf(error)} (in ${document.tex})` });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const { realpath } = await import('node:fs/promises');
|
|
124
|
+
const roots = [.../* @__PURE__ */ new Set([rootDir, await realpath(rootDir).catch(() => rootDir)])];
|
|
125
|
+
for (const problem of problems) {
|
|
126
|
+
for (const root of roots) problem.message = problem.message.replaceAll(`${root}${sep}`, "");
|
|
127
|
+
problem.message = problem.message.replace(/^(template [\w-]+\.eta: )cv-tex\/templates\/[\w-]+\.eta: /, "$1");
|
|
128
|
+
}
|
|
129
|
+
const relevant = only?.length ? problems.filter((problem) => problem.section !== void 0 && only.includes(problem.section) || problem.where === "content/profile.yml") : problems;
|
|
130
|
+
return { problems: relevant, inventory: inv };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export { validateProject };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { parse } from '@vue/compiler-sfc';
|
|
2
|
+
import { l as lineStart, a as lineEnd, i as indentationAt, b as insertLinesAfter, c as insertLinesBefore } from '../shared/vitakit.UfoXdL7U.mjs';
|
|
3
|
+
|
|
4
|
+
const ELEMENT = 1;
|
|
5
|
+
const ATTRIBUTE = 6;
|
|
6
|
+
const pascal = (name) => name.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join("");
|
|
7
|
+
function literalAttribute(node, name) {
|
|
8
|
+
for (const prop of node.props ?? []) {
|
|
9
|
+
if (prop.type === ATTRIBUTE && prop.name === name) return prop.value?.content;
|
|
10
|
+
if (prop.type !== ATTRIBUTE && prop.name === "bind" && prop.arg?.content === name) {
|
|
11
|
+
return /^\s*(['"`])([^'"`$]*)\1\s*$/.exec(prop.exp?.content ?? "")?.[2];
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
return void 0;
|
|
15
|
+
}
|
|
16
|
+
const hasContent = (node) => (node.children ?? []).some((child) => child.type !== 3 && !(child.type === 2 && !child.content.trim()));
|
|
17
|
+
const directive = (node, ...names) => (node?.props ?? []).some((prop) => prop.type !== ATTRIBUTE && names.includes(prop.name));
|
|
18
|
+
function sectionElements(source, components = []) {
|
|
19
|
+
const known = /* @__PURE__ */ new Set(["VitaSection", ...components]);
|
|
20
|
+
let ast;
|
|
21
|
+
try {
|
|
22
|
+
const { descriptor, errors } = parse(source);
|
|
23
|
+
if (errors.length) return { ok: false, reason: `cannot be parsed: ${String(errors[0])}` };
|
|
24
|
+
ast = descriptor.template?.ast;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
return { ok: false, reason: `cannot be parsed: ${error instanceof Error ? error.message : String(error)}` };
|
|
27
|
+
}
|
|
28
|
+
if (!ast) return { ok: false, reason: "it has no <template>" };
|
|
29
|
+
const roots = (ast.children ?? []).filter((node) => node.type === ELEMENT);
|
|
30
|
+
const root = roots.length === 1 ? roots[0] : void 0;
|
|
31
|
+
const elements = [];
|
|
32
|
+
const lists = [];
|
|
33
|
+
const visit = (node, anchor) => {
|
|
34
|
+
if (node.type === ELEMENT && node.tag) {
|
|
35
|
+
const tag = node.tag.includes("-") ? pascal(node.tag) : node.tag;
|
|
36
|
+
const found = (id2) => ({
|
|
37
|
+
id: id2,
|
|
38
|
+
tag: node.tag,
|
|
39
|
+
start: node.loc.start.offset,
|
|
40
|
+
end: node.loc.end.offset,
|
|
41
|
+
line: node.loc.start.line,
|
|
42
|
+
hasContent: hasContent(node),
|
|
43
|
+
anchor: anchor ?? { start: node.loc.start.offset, end: node.loc.end.offset, line: node.loc.start.line, chained: false }
|
|
44
|
+
});
|
|
45
|
+
const id = known.has(tag) ? literalAttribute(node, "id") : void 0;
|
|
46
|
+
if (id) elements.push(found(id));
|
|
47
|
+
const list = tag === "VitaPublications" ? literalAttribute(node, "list") : void 0;
|
|
48
|
+
if (list) lists.push(found(list));
|
|
49
|
+
}
|
|
50
|
+
const children = (node.children ?? []).filter((child) => child.type === ELEMENT);
|
|
51
|
+
for (const [index, child] of children.entries()) {
|
|
52
|
+
const block = node === root ? { start: child.loc.start.offset, end: child.loc.end.offset, line: child.loc.start.line, chained: directive(children[index + 1], "else", "else-if") } : anchor;
|
|
53
|
+
visit(child, block);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
visit(ast);
|
|
57
|
+
return { ok: true, value: { elements, lists, ...root ? { root } : {} } };
|
|
58
|
+
}
|
|
59
|
+
function attribute(value) {
|
|
60
|
+
return value.replace(/&/g, "&").replace(/"/g, """);
|
|
61
|
+
}
|
|
62
|
+
function sectionElementSource(placement) {
|
|
63
|
+
return `<${placement.component ?? "VitaSection"} id="${placement.id}"${placement.title ? ` title="${attribute(placement.title)}"` : ""} />`;
|
|
64
|
+
}
|
|
65
|
+
function publicationsElementSource(list, title) {
|
|
66
|
+
return `<VitaPublications list="${attribute(list)}"${title ? ` title="${attribute(title)}"` : ""} />`;
|
|
67
|
+
}
|
|
68
|
+
function insertElement(source, markup, anchors, root, after) {
|
|
69
|
+
const sorted = [...anchors].sort((a, b) => a.start - b.start);
|
|
70
|
+
const found = (after && sorted.find((existing) => existing.id === after)) ?? sorted[sorted.length - 1];
|
|
71
|
+
if (found) {
|
|
72
|
+
const { anchor } = found;
|
|
73
|
+
if (anchor.chained) return { ok: false, reason: `a v-else follows line ${anchor.line}; a line after it would split the v-if from its v-else` };
|
|
74
|
+
if (source.slice(lineStart(source, anchor.start), anchor.start).trim() || source.slice(anchor.end, lineEnd(source, anchor.end)).trim()) {
|
|
75
|
+
return { ok: false, reason: `line ${anchor.line} holds other markup too` };
|
|
76
|
+
}
|
|
77
|
+
const indent2 = indentationAt(source, anchor.start);
|
|
78
|
+
return { ok: true, value: { source: insertLinesAfter(source, anchor.end - 1, [`${indent2}${markup}`]), afterSection: found.id } };
|
|
79
|
+
}
|
|
80
|
+
if (!root || !root.tag) return { ok: false, reason: "its template has no single root element to add to" };
|
|
81
|
+
const close = source.lastIndexOf(`</${root.tag}`, root.loc.end.offset);
|
|
82
|
+
if (close === -1 || close < root.loc.start.offset || source.slice(lineStart(source, close), close).trim() !== "") {
|
|
83
|
+
return { ok: false, reason: `its <${root.tag}> does not end on a line of its own` };
|
|
84
|
+
}
|
|
85
|
+
const indent = `${indentationAt(source, close)} `;
|
|
86
|
+
return { ok: true, value: { source: insertLinesBefore(source, close, ["", `${indent}${markup}`]) } };
|
|
87
|
+
}
|
|
88
|
+
function addSectionToPage(source, placement, components = []) {
|
|
89
|
+
const found = sectionElements(source, components);
|
|
90
|
+
if (!found.ok) return found;
|
|
91
|
+
const { elements, lists, root } = found.value;
|
|
92
|
+
if (elements.some((element) => element.id === placement.id)) return { ok: false, reason: `it already shows ${placement.id}` };
|
|
93
|
+
const anchors = [...elements, ...lists.filter((list) => list.id === placement.after && !elements.some((element) => element.id === list.id))];
|
|
94
|
+
return insertElement(source, sectionElementSource(placement), anchors, root, placement.after);
|
|
95
|
+
}
|
|
96
|
+
function addPublicationsToPage(source, placement, components = []) {
|
|
97
|
+
const found = sectionElements(source, components);
|
|
98
|
+
if (!found.ok) return found;
|
|
99
|
+
const { elements, lists, root } = found.value;
|
|
100
|
+
if (lists.some((list) => list.id === placement.list)) return { ok: false, reason: `it already shows the list ${placement.list}` };
|
|
101
|
+
return insertElement(source, publicationsElementSource(placement.list, placement.title), [...elements, ...lists], root, placement.after);
|
|
102
|
+
}
|
|
103
|
+
function useComponentForSection(source, id, component, components = []) {
|
|
104
|
+
const found = sectionElements(source, components);
|
|
105
|
+
if (!found.ok) return found;
|
|
106
|
+
const elements = found.value.elements.filter((element) => element.id === id);
|
|
107
|
+
if (!elements.length) return { ok: false, reason: `it does not show ${id}` };
|
|
108
|
+
const blocked = elements.find((element) => element.hasContent);
|
|
109
|
+
if (blocked) return { ok: false, reason: `line ${blocked.line} has content of its own, such as slots; move it into the component` };
|
|
110
|
+
const changed = [];
|
|
111
|
+
let result = source;
|
|
112
|
+
for (const element of [...elements].reverse()) {
|
|
113
|
+
if (element.tag === component) continue;
|
|
114
|
+
let text = result.slice(element.start, element.end);
|
|
115
|
+
text = `<${component}${text.slice(element.tag.length + 1)}`;
|
|
116
|
+
if (text.endsWith(`</${element.tag}>`)) text = `${text.slice(0, -(element.tag.length + 3))}</${component}>`;
|
|
117
|
+
result = result.slice(0, element.start) + text + result.slice(element.end);
|
|
118
|
+
changed.unshift(element.line);
|
|
119
|
+
}
|
|
120
|
+
return { ok: true, value: { source: result, changed } };
|
|
121
|
+
}
|
|
122
|
+
function publicationsElements(source) {
|
|
123
|
+
let ast;
|
|
124
|
+
try {
|
|
125
|
+
const { descriptor, errors } = parse(source);
|
|
126
|
+
if (errors.length) return { ok: false, reason: `cannot be parsed: ${String(errors[0])}` };
|
|
127
|
+
ast = descriptor.template?.ast;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
return { ok: false, reason: `cannot be parsed: ${error instanceof Error ? error.message : String(error)}` };
|
|
130
|
+
}
|
|
131
|
+
const found = [];
|
|
132
|
+
const visit = (node) => {
|
|
133
|
+
if (node.type === ELEMENT && node.tag && (node.tag.includes("-") ? pascal(node.tag) : node.tag) === "VitaPublications") {
|
|
134
|
+
found.push({
|
|
135
|
+
tag: node.tag,
|
|
136
|
+
start: node.loc.start.offset,
|
|
137
|
+
line: node.loc.start.line,
|
|
138
|
+
hasContent: hasContent(node),
|
|
139
|
+
props: (node.props ?? []).map((prop) => ({
|
|
140
|
+
name: prop.type === ATTRIBUTE ? prop.name : prop.name === "bind" && prop.arg ? prop.arg.content : `v-${prop.name}`,
|
|
141
|
+
start: prop.loc.start.offset,
|
|
142
|
+
end: prop.loc.end.offset,
|
|
143
|
+
...prop.type === ATTRIBUTE ? prop.value ? { value: prop.value.content } : {} : { expression: prop.exp?.content ?? "" }
|
|
144
|
+
}))
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
for (const child of node.children ?? []) visit(child);
|
|
148
|
+
};
|
|
149
|
+
if (ast) visit(ast);
|
|
150
|
+
return { ok: true, value: found };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { addPublicationsToPage, addSectionToPage, publicationsElementSource, publicationsElements, sectionElementSource, sectionElements, useComponentForSection };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { parseDocument, isMap, isScalar, isSeq } from 'yaml';
|
|
2
|
+
import { l as lineStart, a as lineEnd } from '../shared/vitakit.UfoXdL7U.mjs';
|
|
3
|
+
|
|
4
|
+
function yamlScalar(value) {
|
|
5
|
+
return /^[A-Za-z0-9/][\w ./()'-]*$/.test(value) && !/(^|\s)#|:\s|\s$/.test(value) ? value : JSON.stringify(value);
|
|
6
|
+
}
|
|
7
|
+
function addNavItem(source, item) {
|
|
8
|
+
const document = parseDocument(source);
|
|
9
|
+
if (document.errors.length) return { ok: false, reason: `cannot be parsed: ${document.errors[0].message}` };
|
|
10
|
+
if (!isMap(document.contents)) return { ok: false, reason: "it is not a map of settings" };
|
|
11
|
+
const entry = `- { label: ${yamlScalar(item.label)}, to: ${yamlScalar(item.to)} }`;
|
|
12
|
+
const nav = document.contents.get("nav", true);
|
|
13
|
+
if (nav === void 0) {
|
|
14
|
+
const ending = source.endsWith("\n") ? "" : "\n";
|
|
15
|
+
return { ok: true, value: `${source}${ending}
|
|
16
|
+
nav:
|
|
17
|
+
${entry}
|
|
18
|
+
` };
|
|
19
|
+
}
|
|
20
|
+
if (!isSeq(nav) || nav.flow) return { ok: false, reason: "its nav is not a list with one entry per line" };
|
|
21
|
+
if (nav.items.some((existing) => isMap(existing) && existing.get("to") === item.to)) {
|
|
22
|
+
return { ok: false, reason: `its menu already links ${item.to}` };
|
|
23
|
+
}
|
|
24
|
+
const last = nav.items[nav.items.length - 1];
|
|
25
|
+
if (!last?.range) return { ok: false, reason: "its nav is empty; add the first entry by hand" };
|
|
26
|
+
const start = lineStart(source, last.range[0]);
|
|
27
|
+
const prefix = /^[ \t]*-[ \t]*/.exec(source.slice(start))?.[0];
|
|
28
|
+
if (!prefix) return { ok: false, reason: "its nav entries are not one per line" };
|
|
29
|
+
const end = lineEnd(source, last.range[1] - 1);
|
|
30
|
+
const indent = prefix.slice(0, prefix.indexOf("-"));
|
|
31
|
+
return { ok: true, value: `${source.slice(0, end)}
|
|
32
|
+
${indent}${entry}${source.slice(end)}` };
|
|
33
|
+
}
|
|
34
|
+
function flow(value) {
|
|
35
|
+
if (Array.isArray(value)) return `[${value.map(flow).join(", ")}]`;
|
|
36
|
+
if (value && typeof value === "object") return `{ ${Object.entries(value).map(([key, item]) => `${yamlScalar(key)}: ${flow(item)}`).join(", ")} }`;
|
|
37
|
+
return typeof value === "string" ? yamlScalar(value) : String(value);
|
|
38
|
+
}
|
|
39
|
+
function publicationListYaml(name, settings) {
|
|
40
|
+
const lines = Object.entries(settings).filter(([, value]) => value !== void 0).map(([key, value]) => ` ${key}: ${flow(value)}`);
|
|
41
|
+
return lines.length ? `${name}:
|
|
42
|
+
${lines.join("\n")}` : `${name}: {}`;
|
|
43
|
+
}
|
|
44
|
+
function publicationListLine(source, name) {
|
|
45
|
+
const document = parseDocument(source);
|
|
46
|
+
const pair = isMap(document.contents) ? document.contents.items.find((item) => isScalar(item.key) && item.key.value === name) : void 0;
|
|
47
|
+
const offset = pair?.key?.range?.[0];
|
|
48
|
+
return offset === void 0 ? void 0 : source.slice(0, offset).split("\n").length;
|
|
49
|
+
}
|
|
50
|
+
function addPublicationList(source, name, settings) {
|
|
51
|
+
const document = parseDocument(source);
|
|
52
|
+
if (document.errors.length) return { ok: false, reason: `cannot be parsed: ${document.errors[0].message}` };
|
|
53
|
+
if (document.contents !== null && !isMap(document.contents)) return { ok: false, reason: "it is not one list per name" };
|
|
54
|
+
if (isMap(document.contents) && document.contents.flow) return { ok: false, reason: "its lists are written on one line" };
|
|
55
|
+
const line = publicationListLine(source, name);
|
|
56
|
+
if (line !== void 0) return { ok: false, reason: `it has a list "${name}" already, on line ${line}` };
|
|
57
|
+
const body = source.replace(/\s*$/, "");
|
|
58
|
+
return { ok: true, value: `${body}${body ? "\n\n" : ""}${publicationListYaml(name, settings)}
|
|
59
|
+
` };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { addNavItem, addPublicationList, publicationListLine, publicationListYaml, yamlScalar };
|
package/dist/cli.d.mts
CHANGED
|
@@ -1,45 +1,38 @@
|
|
|
1
|
-
interface
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
interface Io {
|
|
2
|
+
cwd: string;
|
|
3
|
+
env: NodeJS.ProcessEnv;
|
|
4
|
+
/** Write to stdout; text is written as is, add `\n` yourself. */
|
|
5
|
+
out: (text: string) => void;
|
|
6
|
+
err: (text: string) => void;
|
|
7
|
+
/** Whether a person can answer questions: stdin and stdout are terminals. */
|
|
8
|
+
interactive: boolean;
|
|
9
|
+
/** Whether stdout is a terminal, for colour. */
|
|
10
|
+
tty: boolean;
|
|
11
|
+
/** Width of the terminal, when stdout is one. */
|
|
12
|
+
columns?: number;
|
|
13
|
+
/** Ask a question and return the answer; only called when `interactive`. */
|
|
14
|
+
ask: (question: string) => Promise<string>;
|
|
8
15
|
}
|
|
9
|
-
interface WrittenFile {
|
|
10
|
-
path: string;
|
|
11
|
-
/** Why it was written, for the command's output. */
|
|
12
|
-
note: string;
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* Create what is missing for a section: the YAML file, the schema, optionally
|
|
16
|
-
* a component. Existing files are never touched.
|
|
17
|
-
*/
|
|
18
|
-
declare function addSection(id: string, rootDir: string, options?: AddOptions): Promise<WrittenFile[]>;
|
|
19
16
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
kind: 'component' | 'template' | 'class' | 'schema' | 'style';
|
|
24
|
-
/** Absolute source path inside the package. */
|
|
25
|
-
source: string;
|
|
26
|
-
/** Destination relative to the project root. */
|
|
27
|
-
target: string;
|
|
28
|
-
}
|
|
29
|
-
/** Everything that can be copied into a project. */
|
|
30
|
-
declare function listEjectable(): Promise<EjectableItem[]>;
|
|
31
|
-
interface EjectResult {
|
|
32
|
-
item: EjectableItem;
|
|
33
|
-
/** Absolute path written. */
|
|
34
|
-
path: string;
|
|
35
|
-
}
|
|
17
|
+
declare function main(argv: string[], io?: Io): Promise<number>;
|
|
18
|
+
|
|
19
|
+
type PackageManager = 'pnpm' | 'npm';
|
|
36
20
|
/**
|
|
37
|
-
*
|
|
38
|
-
*
|
|
21
|
+
* The package manager that started this process, from the user agent npm and
|
|
22
|
+
* pnpm set for everything they run (`npm run`, `npx`, `pnpm <script>`,
|
|
23
|
+
* `pnpm exec`); anything but npm gets pnpm, the default, as in the creator.
|
|
24
|
+
* A binary run directly has no user agent; the project's lockfile decides.
|
|
39
25
|
*/
|
|
40
|
-
declare function
|
|
41
|
-
|
|
42
|
-
|
|
26
|
+
declare function detectPackageManager(rootDir?: string, env?: NodeJS.ProcessEnv): PackageManager;
|
|
27
|
+
interface Commands {
|
|
28
|
+
/** A script from package.json: `pnpm pdf cv`, `npm run pdf -- cv`. */
|
|
29
|
+
script: (name: string, args?: string) => string;
|
|
30
|
+
/** A binary of an installed package: `pnpm vitakit status`, `npx vitakit status`. */
|
|
31
|
+
bin: (name: string, args?: string) => string;
|
|
32
|
+
}
|
|
33
|
+
declare function commands(packageManager?: PackageManager): Commands;
|
|
34
|
+
/** Lines of `command # comment`, with the comments aligned. */
|
|
35
|
+
declare function aligned(rows: [command: string, comment?: string][], indent?: string): string;
|
|
43
36
|
|
|
44
|
-
export {
|
|
45
|
-
export type {
|
|
37
|
+
export { aligned, commands, detectPackageManager, main };
|
|
38
|
+
export type { Commands, Io, PackageManager };
|
package/dist/cli.mjs
CHANGED
|
@@ -1,225 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import { b as bundledTexDir } from './shared/vitakit.BRhjqpAt.mjs';
|
|
6
|
-
import { parse } from 'yaml';
|
|
1
|
+
export { a as aligned, c as commands, d as detectPackageManager, m as main } from './shared/vitakit.zwr1ssO5.mjs';
|
|
2
|
+
import 'node:fs';
|
|
3
|
+
import 'node:path';
|
|
4
|
+
import './shared/vitakit.DQ6PmGoL.mjs';
|
|
7
5
|
import 'node:url';
|
|
8
|
-
import '
|
|
9
|
-
import '
|
|
10
|
-
import '
|
|
11
|
-
import './shared/vitakit.BlU-p01e.mjs';
|
|
12
|
-
|
|
13
|
-
const DATE_LIKE = /^\d{4}(-\d{2}){0,2}$/;
|
|
14
|
-
function pascalCase(id) {
|
|
15
|
-
return id.split("-").map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
16
|
-
}
|
|
17
|
-
function inferField(values, present, total) {
|
|
18
|
-
const optional = present < total ? ".optional()" : "";
|
|
19
|
-
const sample = values[0];
|
|
20
|
-
if (typeof sample === "boolean") return `z.boolean().default(false)`;
|
|
21
|
-
if (typeof sample === "number" && !DATE_LIKE.test(String(sample))) return `z.number()${optional}`;
|
|
22
|
-
if (values.every((value) => DATE_LIKE.test(String(value)))) return `dateRange${optional}`;
|
|
23
|
-
if (typeof sample === "object" && sample !== null && "from" in sample) return `dateRange${optional}`;
|
|
24
|
-
if (typeof sample === "string" && /^https?:\/\//.test(sample)) return `url${optional}`;
|
|
25
|
-
const strings = values.filter((value) => typeof value === "string");
|
|
26
|
-
const unique = [...new Set(strings)];
|
|
27
|
-
if (strings.length === values.length && unique.length > 1 && unique.length <= Math.max(2, values.length / 2) && unique.every((value) => /^[a-z][\w-]*$/.test(value))) {
|
|
28
|
-
return `z.enum([${unique.map((value) => `'${value}'`).join(", ")}])${optional}`;
|
|
29
|
-
}
|
|
30
|
-
if (typeof sample === "string") return `text${optional}`;
|
|
31
|
-
return `z.unknown()${optional}`;
|
|
32
|
-
}
|
|
33
|
-
function inferSchema(entries) {
|
|
34
|
-
const fields = /* @__PURE__ */ new Map();
|
|
35
|
-
for (const entry of entries) {
|
|
36
|
-
for (const [key, value] of Object.entries(entry)) {
|
|
37
|
-
if (key === "visibility") continue;
|
|
38
|
-
if (!fields.has(key)) fields.set(key, []);
|
|
39
|
-
if (value !== void 0 && value !== null) fields.get(key).push(value);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const used = /* @__PURE__ */ new Set(["section"]);
|
|
43
|
-
const lines = [];
|
|
44
|
-
for (const [key, values] of fields) {
|
|
45
|
-
const present = entries.filter((entry) => entry[key] !== void 0).length;
|
|
46
|
-
let type = inferField(values, present, entries.length);
|
|
47
|
-
if (/^(title|headline)$/.test(key) && type.startsWith("text")) type = type.replace("text", "richText");
|
|
48
|
-
for (const primitive of ["dateRange", "richText", "text", "url", "z"]) {
|
|
49
|
-
if (new RegExp(`\\b${primitive}\\b`).test(type)) used.add(primitive);
|
|
50
|
-
}
|
|
51
|
-
lines.push(` ${key}: ${type},`);
|
|
52
|
-
}
|
|
53
|
-
return { body: lines.join("\n"), imports: [...used].sort() };
|
|
54
|
-
}
|
|
55
|
-
const SAMPLE_YAML = (id) => `# ${id}: one entry per item, newest first is not required --
|
|
56
|
-
# vitakit sorts by date for you. Edit the fields, then edit schemas/${id}.ts
|
|
57
|
-
# to match.
|
|
58
|
-
- date: 2025-03-14
|
|
59
|
-
title: An example entry
|
|
60
|
-
note: Replace this with something real.
|
|
61
|
-
`;
|
|
62
|
-
function schemaModule(id, body, imports, inferred) {
|
|
63
|
-
return [
|
|
64
|
-
`/**`,
|
|
65
|
-
` * Shape of content/${id}.yml.`,
|
|
66
|
-
` *`,
|
|
67
|
-
inferred ? ` * Drafted from the existing file: check the types, especially which fields
|
|
68
|
-
* are optional and whether any should be an enum.` : ` * Edit freely: this file defines what content/${id}.yml may contain.`,
|
|
69
|
-
` *`,
|
|
70
|
-
` * \`section()\` adds \`visibility\` and validates a list of entries.`,
|
|
71
|
-
` */`,
|
|
72
|
-
`import { ${imports.join(", ")} } from '@anja.codes/vitakit'`,
|
|
73
|
-
``,
|
|
74
|
-
`export default section({`,
|
|
75
|
-
body || ` date: dateRange,
|
|
76
|
-
title: richText,`,
|
|
77
|
-
`})`,
|
|
78
|
-
``
|
|
79
|
-
].join("\n");
|
|
80
|
-
}
|
|
81
|
-
function componentModule(id) {
|
|
82
|
-
`${pascalCase(id)}Section`;
|
|
83
|
-
return `<script setup lang="ts">
|
|
84
|
-
/**
|
|
85
|
-
* How ${id} entries look. Yours to change: props in, markup out.
|
|
86
|
-
*
|
|
87
|
-
* \`useSection('${id}')\` returns validated, sorted entries; \`VitaEntry\`,
|
|
88
|
-
* \`VitaDate\` and \`richHtml\` come from vitakit.
|
|
89
|
-
*/
|
|
90
|
-
import { richHtml, type Entry } from '@anja.codes/vitakit/shared'
|
|
91
|
-
|
|
92
|
-
const props = defineProps<{ entries?: Entry[], limit?: number }>()
|
|
93
|
-
|
|
94
|
-
const items = computed(() => {
|
|
95
|
-
const source = props.entries ?? useSection('${id}')
|
|
96
|
-
return props.limit ? source.slice(0, props.limit) : source
|
|
97
|
-
})
|
|
98
|
-
<\/script>
|
|
99
|
-
|
|
100
|
-
<template>
|
|
101
|
-
<ul v-if="items.length" class="vita-section">
|
|
102
|
-
<VitaEntry v-for="item in items" :key="item.id" :date="item.date as never">
|
|
103
|
-
<p class="vita-entry__title" v-html="richHtml(item.title)" />
|
|
104
|
-
<p v-if="typeof item.note === 'string'" class="vita-entry__note">
|
|
105
|
-
{{ item.note }}
|
|
106
|
-
</p>
|
|
107
|
-
</VitaEntry>
|
|
108
|
-
</ul>
|
|
109
|
-
</template>
|
|
110
|
-
`;
|
|
111
|
-
}
|
|
112
|
-
async function addSection(id, rootDir, options = {}) {
|
|
113
|
-
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(id)) {
|
|
114
|
-
throw new Error(`"${id}" must be lowercase kebab-case, e.g. research-stays`);
|
|
115
|
-
}
|
|
116
|
-
if (id in schemas) {
|
|
117
|
-
throw new Error(
|
|
118
|
-
`"${id}" is a built-in section: create content/${id}.yml and it works.
|
|
119
|
-
To change which fields it may contain, copy its schema into your project:
|
|
120
|
-
pnpm vitakit eject ${id}
|
|
121
|
-
and edit schemas/${id}.ts.`
|
|
122
|
-
);
|
|
123
|
-
}
|
|
124
|
-
const written = [];
|
|
125
|
-
const yamlPath = join(rootDir, "content", `${id}.yml`);
|
|
126
|
-
const schemaPath = join(rootDir, "schemas", `${id}.ts`);
|
|
127
|
-
let entries = [];
|
|
128
|
-
let inferred = false;
|
|
129
|
-
if (existsSync(yamlPath)) {
|
|
130
|
-
const parsed = parse(await readFile(yamlPath, "utf8"));
|
|
131
|
-
if (!Array.isArray(parsed)) throw new Error(`content/${id}.yml must contain a list of entries`);
|
|
132
|
-
entries = parsed.filter((entry) => typeof entry === "object" && entry !== null);
|
|
133
|
-
if (!entries.length) throw new Error(`content/${id}.yml has no entries to infer a schema from`);
|
|
134
|
-
inferred = true;
|
|
135
|
-
} else {
|
|
136
|
-
await mkdir(join(yamlPath, ".."), { recursive: true });
|
|
137
|
-
await writeFile(yamlPath, SAMPLE_YAML(id));
|
|
138
|
-
written.push({ path: yamlPath, note: "your data \u2014 edit this first" });
|
|
139
|
-
entries = [{ date: "2025-03-14", title: "An example entry", note: "Replace this." }];
|
|
140
|
-
}
|
|
141
|
-
if (existsSync(schemaPath)) {
|
|
142
|
-
throw new Error(`schemas/${id}.ts already exists; delete it first to regenerate`);
|
|
143
|
-
}
|
|
144
|
-
const { body, imports } = inferSchema(entries);
|
|
145
|
-
await mkdir(join(schemaPath, ".."), { recursive: true });
|
|
146
|
-
await writeFile(schemaPath, schemaModule(id, body, imports, inferred));
|
|
147
|
-
written.push({ path: schemaPath, note: inferred ? `drafted from content/${id}.yml` : "the shape of an entry" });
|
|
148
|
-
if (options.component !== false) {
|
|
149
|
-
const componentPath = join(rootDir, "app", "components", `${pascalCase(id)}Section.vue`);
|
|
150
|
-
if (!existsSync(componentPath)) {
|
|
151
|
-
await mkdir(join(componentPath, ".."), { recursive: true });
|
|
152
|
-
await writeFile(componentPath, componentModule(id));
|
|
153
|
-
written.push({ path: componentPath, note: "how the entries look" });
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
return written.map((file) => ({ ...file, path: relative(rootDir, file.path) }));
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
async function listEjectable() {
|
|
160
|
-
const items = [];
|
|
161
|
-
const componentsDir = resolveAsset("runtime/components");
|
|
162
|
-
for (const file of (await readdir(componentsDir)).filter((name) => name.endsWith(".vue")).sort()) {
|
|
163
|
-
items.push({
|
|
164
|
-
name: basename(file, ".vue"),
|
|
165
|
-
kind: "component",
|
|
166
|
-
source: join(componentsDir, file),
|
|
167
|
-
target: join("app", "components", file)
|
|
168
|
-
});
|
|
169
|
-
}
|
|
170
|
-
const templatesDir = join(bundledTexDir, "templates");
|
|
171
|
-
for (const file of (await readdir(templatesDir)).filter((name) => name.endsWith(".eta")).sort()) {
|
|
172
|
-
items.push({
|
|
173
|
-
name: file,
|
|
174
|
-
kind: "template",
|
|
175
|
-
source: join(templatesDir, file),
|
|
176
|
-
target: join("cv-tex", "templates", file)
|
|
177
|
-
});
|
|
178
|
-
}
|
|
179
|
-
const schemaDir = resolveAsset("schemas");
|
|
180
|
-
for (const file of (await readdir(schemaDir)).filter((name) => name.endsWith(".ts")).sort()) {
|
|
181
|
-
const id = basename(file, ".ts");
|
|
182
|
-
items.push({
|
|
183
|
-
name: id,
|
|
184
|
-
kind: "schema",
|
|
185
|
-
source: join(schemaDir, file),
|
|
186
|
-
target: join("schemas", file)
|
|
187
|
-
});
|
|
188
|
-
}
|
|
189
|
-
items.push({
|
|
190
|
-
name: "publications.csl",
|
|
191
|
-
kind: "style",
|
|
192
|
-
// The path of `defaultCslStyle`, without loading citation-js to get it.
|
|
193
|
-
source: join(resolveAsset("styles"), "vitakit-math.csl"),
|
|
194
|
-
target: join("content", "publications.csl")
|
|
195
|
-
});
|
|
196
|
-
items.push({
|
|
197
|
-
name: "vitakit-cv.cls",
|
|
198
|
-
kind: "class",
|
|
199
|
-
source: join(bundledTexDir, "vitakit-cv.cls"),
|
|
200
|
-
target: join("cv-tex", "vitakit-cv.cls")
|
|
201
|
-
});
|
|
202
|
-
return items;
|
|
203
|
-
}
|
|
204
|
-
async function eject(name, rootDir, { force = false } = {}) {
|
|
205
|
-
const items = await listEjectable();
|
|
206
|
-
const item = items.find((candidate) => candidate.name === name) ?? items.find((candidate) => candidate.name.toLowerCase() === name.toLowerCase());
|
|
207
|
-
if (!item) {
|
|
208
|
-
const known = items.map((candidate) => candidate.name).join(", ");
|
|
209
|
-
throw new Error(`unknown item "${name}". Available: ${known}`);
|
|
210
|
-
}
|
|
211
|
-
const path = join(rootDir, item.target);
|
|
212
|
-
if (existsSync(path) && !force) {
|
|
213
|
-
throw new Error(`${relative(rootDir, path)} already exists; pass --force to overwrite`);
|
|
214
|
-
}
|
|
215
|
-
await mkdir(join(path, ".."), { recursive: true });
|
|
216
|
-
if (item.kind === "schema") {
|
|
217
|
-
const source = await readFile(item.source, "utf8");
|
|
218
|
-
await writeFile(path, source.replace(/from '\.\.\/[\w-]+'/g, "from '@anja.codes/vitakit'"));
|
|
219
|
-
} else {
|
|
220
|
-
await copyFile(item.source, path);
|
|
221
|
-
}
|
|
222
|
-
return { item, path };
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export { addSection, eject, listEjectable };
|
|
6
|
+
import 'node:process';
|
|
7
|
+
import 'node:util';
|
|
8
|
+
import 'node:readline/promises';
|