@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
|
@@ -31,7 +31,7 @@ interface DateRange {
|
|
|
31
31
|
* Intermediate form of a `richText` field, before rendering.
|
|
32
32
|
*
|
|
33
33
|
* Internal: it exists only between validation and `renderRichFields`, never in
|
|
34
|
-
* a `VitakitContent`. `vitakit
|
|
34
|
+
* a `VitakitContent`. `vitakit verify` fails the build if one reaches the output.
|
|
35
35
|
* Read rich fields with `richSource` / `richHtml` instead of inspecting shapes.
|
|
36
36
|
*/
|
|
37
37
|
interface RichTextMarker {
|
|
@@ -44,14 +44,15 @@ interface RichText {
|
|
|
44
44
|
/** Rendered inline HTML; use with `v-html`. */
|
|
45
45
|
html: string;
|
|
46
46
|
}
|
|
47
|
-
|
|
47
|
+
/** A label that may link: an institution, a funder, a person. See `linked`. */
|
|
48
|
+
interface Linked {
|
|
48
49
|
label: string;
|
|
49
|
-
url: string;
|
|
50
|
-
}
|
|
51
|
-
interface Institution {
|
|
52
|
-
name: string;
|
|
53
50
|
url?: string;
|
|
54
51
|
}
|
|
52
|
+
/** A label that always links. */
|
|
53
|
+
interface Link extends Linked {
|
|
54
|
+
url: string;
|
|
55
|
+
}
|
|
55
56
|
/** Fields every section entry has, whatever else its schema adds. */
|
|
56
57
|
interface EntryBase {
|
|
57
58
|
/** Stable identity for `v-for` keys: the source file plus the index. */
|
|
@@ -90,17 +91,28 @@ interface Publication {
|
|
|
90
91
|
*/
|
|
91
92
|
fields: Record<string, string>;
|
|
92
93
|
}
|
|
93
|
-
/** One group of
|
|
94
|
+
/** One group of a publication list, e.g. its preprints or its 2024 papers. */
|
|
94
95
|
interface PublicationGroup {
|
|
96
|
+
/** Made from the list name, `recent-1`; safe as an HTML id and in LaTeX. */
|
|
95
97
|
id: string;
|
|
96
|
-
|
|
98
|
+
/** The group's heading; none for the single group of an ungrouped list. */
|
|
99
|
+
label?: string;
|
|
97
100
|
count: number;
|
|
98
101
|
/**
|
|
99
|
-
*
|
|
102
|
+
* In the list's order. Formatted together, so numbering and year suffixes
|
|
100
103
|
* (2024a, 2024b) are right within the group.
|
|
101
104
|
*/
|
|
102
105
|
entries: Publication[];
|
|
103
106
|
}
|
|
107
|
+
/**
|
|
108
|
+
* A list declared in `content/publication-lists.yml`, selected, sorted,
|
|
109
|
+
* grouped and formatted at build time. An ungrouped list is one group
|
|
110
|
+
* without a label.
|
|
111
|
+
*/
|
|
112
|
+
interface PublicationList {
|
|
113
|
+
count: number;
|
|
114
|
+
groups: PublicationGroup[];
|
|
115
|
+
}
|
|
104
116
|
interface Page {
|
|
105
117
|
/** Route path, with leading and trailing slash: `/`, `/research/`. */
|
|
106
118
|
route: string;
|
|
@@ -117,7 +129,7 @@ interface Page {
|
|
|
117
129
|
interface Profile {
|
|
118
130
|
name: string;
|
|
119
131
|
title?: string;
|
|
120
|
-
affiliation?:
|
|
132
|
+
affiliation?: Linked;
|
|
121
133
|
email?: string;
|
|
122
134
|
links: Link[];
|
|
123
135
|
/** Short plain-text description used for page metadata, not rendered. */
|
|
@@ -145,9 +157,10 @@ interface VitakitContent {
|
|
|
145
157
|
pages: Page[];
|
|
146
158
|
/** Section id -> validated, sorted, visibility-filtered entries. */
|
|
147
159
|
sections: Record<string, Entry[]>;
|
|
148
|
-
publications
|
|
160
|
+
/** List name -> its publications, as `content/publication-lists.yml` declares. */
|
|
161
|
+
publications: Record<string, PublicationList>;
|
|
149
162
|
/** Always empty in a production build, which fails instead. */
|
|
150
163
|
issues: ContentIssue[];
|
|
151
164
|
}
|
|
152
165
|
|
|
153
|
-
export type { ContentIssue as C, DateRange as D, Entry as E,
|
|
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 };
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
|
|
3
|
+
const PACKAGE = "@anja.codes/vitakit";
|
|
4
|
+
function parseModule(source) {
|
|
5
|
+
return parse(source, { sourceType: "module", plugins: ["typescript"] }).program;
|
|
6
|
+
}
|
|
7
|
+
function unwrap(expression) {
|
|
8
|
+
let current = expression;
|
|
9
|
+
while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "ParenthesizedExpression" || current.type === "TSNonNullExpression") {
|
|
10
|
+
current = current.expression;
|
|
11
|
+
}
|
|
12
|
+
return current;
|
|
13
|
+
}
|
|
14
|
+
function packageImports(body) {
|
|
15
|
+
const names = /* @__PURE__ */ new Map();
|
|
16
|
+
for (const statement of body) {
|
|
17
|
+
if (statement.type !== "ImportDeclaration" || statement.source.value !== PACKAGE) continue;
|
|
18
|
+
for (const specifier of statement.specifiers) {
|
|
19
|
+
if (specifier.type !== "ImportSpecifier") continue;
|
|
20
|
+
const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
|
|
21
|
+
names.set(specifier.local.name, imported);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return names;
|
|
25
|
+
}
|
|
26
|
+
function defaultExport(body) {
|
|
27
|
+
const exported = body.find((statement) => statement.type === "ExportDefaultDeclaration");
|
|
28
|
+
if (!exported || exported.type !== "ExportDefaultDeclaration") return void 0;
|
|
29
|
+
const declaration = exported.declaration;
|
|
30
|
+
if (declaration.type === "Identifier") {
|
|
31
|
+
for (const statement of body) {
|
|
32
|
+
if (statement.type !== "VariableDeclaration" || statement.kind !== "const") continue;
|
|
33
|
+
for (const declarator of statement.declarations) {
|
|
34
|
+
if (declarator.id.type === "Identifier" && declarator.id.name === declaration.name && declarator.init) {
|
|
35
|
+
return unwrap(declarator.init);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
if (declaration.type.endsWith("Declaration")) return void 0;
|
|
42
|
+
return unwrap(declaration);
|
|
43
|
+
}
|
|
44
|
+
function keyName(property) {
|
|
45
|
+
if (property.type !== "ObjectProperty" || property.computed) return void 0;
|
|
46
|
+
if (property.key.type === "Identifier") return property.key.name;
|
|
47
|
+
if (property.key.type === "StringLiteral") return property.key.value;
|
|
48
|
+
return void 0;
|
|
49
|
+
}
|
|
50
|
+
function recognizeSchema(source) {
|
|
51
|
+
let body;
|
|
52
|
+
try {
|
|
53
|
+
body = parseModule(source).body;
|
|
54
|
+
} catch (error) {
|
|
55
|
+
return { ok: false, reason: `cannot be parsed: ${error instanceof Error ? error.message : String(error)}` };
|
|
56
|
+
}
|
|
57
|
+
const expression = defaultExport(body);
|
|
58
|
+
if (!expression || expression.type !== "CallExpression" || expression.callee.type !== "Identifier") {
|
|
59
|
+
return { ok: false, reason: "its default export is not a section(...) or extendSection(...) call" };
|
|
60
|
+
}
|
|
61
|
+
const call = expression;
|
|
62
|
+
const kind = packageImports(body).get(call.callee.name);
|
|
63
|
+
const object = kind === "section" ? call.arguments[0] : kind === "extendSection" ? call.arguments[1] : void 0;
|
|
64
|
+
const expected = kind === "section" ? 1 : 2;
|
|
65
|
+
if (kind !== "section" && kind !== "extendSection" || call.arguments.length !== expected || !object || object.type !== "ObjectExpression") {
|
|
66
|
+
return { ok: false, reason: `its default export is not section({ ... }) or extendSection(base, { ... }) from ${PACKAGE}` };
|
|
67
|
+
}
|
|
68
|
+
const fields = [];
|
|
69
|
+
for (const property of object.properties) {
|
|
70
|
+
const name = keyName(property);
|
|
71
|
+
if (name === void 0) return { ok: false, reason: "its fields contain a spread or a computed key" };
|
|
72
|
+
fields.push(name);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
value: {
|
|
77
|
+
kind,
|
|
78
|
+
fields,
|
|
79
|
+
start: object.start,
|
|
80
|
+
end: object.end,
|
|
81
|
+
properties: object.properties.map((property) => ({ start: property.start, end: property.end }))
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
|
|
86
|
+
function propertyKey(name) {
|
|
87
|
+
return IDENTIFIER.test(name) ? name : `'${name.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
|
|
88
|
+
}
|
|
89
|
+
function lineStart(source, offset) {
|
|
90
|
+
return source.lastIndexOf("\n", offset - 1) + 1;
|
|
91
|
+
}
|
|
92
|
+
function indentationAt(source, offset) {
|
|
93
|
+
return /^[ \t]*/.exec(source.slice(lineStart(source, offset)))[0];
|
|
94
|
+
}
|
|
95
|
+
function endOfLineContent(source, offset) {
|
|
96
|
+
let position = offset;
|
|
97
|
+
for (; ; ) {
|
|
98
|
+
while (source[position] === " " || source[position] === " ") position++;
|
|
99
|
+
if (source.startsWith("//", position)) {
|
|
100
|
+
const end = source.indexOf("\n", position);
|
|
101
|
+
return end === -1 ? source.length : end;
|
|
102
|
+
}
|
|
103
|
+
if (source.startsWith("/*", position)) {
|
|
104
|
+
const end = source.indexOf("*/", position + 2);
|
|
105
|
+
if (end === -1 || source.slice(position, end).includes("\n")) return position;
|
|
106
|
+
position = end + 2;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
return position;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function addSchemaField(source, name, code) {
|
|
113
|
+
const recognized = recognizeSchema(source);
|
|
114
|
+
if (!recognized.ok) return recognized;
|
|
115
|
+
const { fields, properties, start: open, end } = recognized.value;
|
|
116
|
+
if (fields.includes(name)) return { ok: false, reason: `it already has a field "${name}"` };
|
|
117
|
+
const entry = `${propertyKey(name)}: ${code}`;
|
|
118
|
+
const close = end - 1;
|
|
119
|
+
if (!properties.length) {
|
|
120
|
+
const inner = source.slice(open + 1, close);
|
|
121
|
+
if (!inner.includes("\n")) {
|
|
122
|
+
const indent3 = indentationAt(source, open);
|
|
123
|
+
return { ok: true, value: `${source.slice(0, open)}{
|
|
124
|
+
${indent3} ${entry},
|
|
125
|
+
${indent3}}${source.slice(close + 1)}` };
|
|
126
|
+
}
|
|
127
|
+
const closeLine = lineStart(source, close);
|
|
128
|
+
const indent2 = `${indentationAt(source, close)} `;
|
|
129
|
+
if (source.slice(closeLine, close).trim() !== "") {
|
|
130
|
+
return { ok: true, value: `${source.slice(0, close)}
|
|
131
|
+
${indent2}${entry},
|
|
132
|
+
${source.slice(close)}` };
|
|
133
|
+
}
|
|
134
|
+
return { ok: true, value: `${source.slice(0, closeLine)}${indent2}${entry},
|
|
135
|
+
${source.slice(closeLine)}` };
|
|
136
|
+
}
|
|
137
|
+
const first = properties[0];
|
|
138
|
+
const last = properties[properties.length - 1];
|
|
139
|
+
const multiline = source.slice(open, close).includes("\n") && source.slice(lineStart(source, first.start), first.start).trim() === "";
|
|
140
|
+
let afterLast = last.end;
|
|
141
|
+
while (source[afterLast] === " " || source[afterLast] === " ") afterLast++;
|
|
142
|
+
const comma = source[afterLast] === ",";
|
|
143
|
+
if (!multiline) {
|
|
144
|
+
return { ok: true, value: `${source.slice(0, last.end)}, ${entry}${source.slice(last.end)}` };
|
|
145
|
+
}
|
|
146
|
+
const indent = indentationAt(source, first.start);
|
|
147
|
+
const insertAt = endOfLineContent(source, comma ? afterLast + 1 : last.end);
|
|
148
|
+
const head = comma ? source.slice(0, insertAt) : `${source.slice(0, last.end)},${source.slice(last.end, insertAt)}`;
|
|
149
|
+
return { ok: true, value: `${head}
|
|
150
|
+
${indent}${entry}${comma ? "," : ""}${source.slice(insertAt)}` };
|
|
151
|
+
}
|
|
152
|
+
function ensurePackageImports(source, names) {
|
|
153
|
+
let body;
|
|
154
|
+
try {
|
|
155
|
+
body = parseModule(source).body;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
return { ok: false, reason: `cannot be parsed: ${error instanceof Error ? error.message : String(error)}` };
|
|
158
|
+
}
|
|
159
|
+
const imported = packageImports(body);
|
|
160
|
+
const local = new Set(imported.keys());
|
|
161
|
+
const missing = names.filter((name) => !local.has(name));
|
|
162
|
+
if (!missing.length) return { ok: true, value: source };
|
|
163
|
+
const declaration = body.find((statement) => statement.type === "ImportDeclaration" && statement.source.value === PACKAGE && statement.importKind !== "type" && statement.specifiers.some((specifier) => specifier.type === "ImportSpecifier"));
|
|
164
|
+
if (declaration) {
|
|
165
|
+
const specifiers = declaration.specifiers.filter((specifier) => specifier.type === "ImportSpecifier");
|
|
166
|
+
const at2 = specifiers[specifiers.length - 1].end;
|
|
167
|
+
return { ok: true, value: `${source.slice(0, at2)}, ${missing.join(", ")}${source.slice(at2)}` };
|
|
168
|
+
}
|
|
169
|
+
const imports = body.filter((statement) => statement.type === "ImportDeclaration");
|
|
170
|
+
const line = `import { ${missing.join(", ")} } from '${PACKAGE}'
|
|
171
|
+
`;
|
|
172
|
+
if (!imports.length) return { ok: true, value: line + source };
|
|
173
|
+
const at = imports[imports.length - 1].end;
|
|
174
|
+
const end = source.indexOf("\n", at);
|
|
175
|
+
const position = end === -1 ? source.length : end + 1;
|
|
176
|
+
return { ok: true, value: `${source.slice(0, position)}${end === -1 ? "\n" : ""}${line}${source.slice(position)}` };
|
|
177
|
+
}
|
|
178
|
+
function renamePublicationsSettings(source, renames) {
|
|
179
|
+
let body;
|
|
180
|
+
try {
|
|
181
|
+
body = parseModule(source).body;
|
|
182
|
+
} catch (error) {
|
|
183
|
+
return { ok: false, reason: `cannot be parsed: ${error instanceof Error ? error.message : String(error)}` };
|
|
184
|
+
}
|
|
185
|
+
const expression = defaultExport(body);
|
|
186
|
+
const object = expression?.type === "CallExpression" && expression.callee.type === "Identifier" && packageImports(body).get(expression.callee.name) === "publications" ? expression.arguments[0] : void 0;
|
|
187
|
+
if (!object || object.type !== "ObjectExpression") {
|
|
188
|
+
return { ok: false, reason: `its default export is not publications({ ... }) from ${PACKAGE}` };
|
|
189
|
+
}
|
|
190
|
+
let result = source;
|
|
191
|
+
for (const property of [...object.properties].reverse()) {
|
|
192
|
+
const name = keyName(property);
|
|
193
|
+
if (name === void 0 || !Object.hasOwn(renames, name) || property.type !== "ObjectProperty") continue;
|
|
194
|
+
result = result.slice(0, property.key.start) + renames[name] + result.slice(property.key.end);
|
|
195
|
+
}
|
|
196
|
+
return { ok: true, value: result };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export { addSchemaField as a, ensurePackageImports as e, propertyKey as p, renamePublicationsSettings as r };
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
function formatPoint(point, { locale = "en", precision = "auto" }) {
|
|
2
|
+
if (precision === "year" || point.month === void 0) return String(point.year);
|
|
3
|
+
const date = new Date(Date.UTC(point.year, point.month - 1, point.day ?? 1));
|
|
4
|
+
return new Intl.DateTimeFormat(locale, {
|
|
5
|
+
year: "numeric",
|
|
6
|
+
month: "short",
|
|
7
|
+
...point.day === void 0 ? {} : { day: "numeric" },
|
|
8
|
+
timeZone: "UTC"
|
|
9
|
+
}).format(date);
|
|
10
|
+
}
|
|
11
|
+
function formatDateRange(range, options = {}) {
|
|
12
|
+
if (!range) return "";
|
|
13
|
+
const { presentLabel = "present" } = options;
|
|
14
|
+
const from = formatPoint(range.from, options);
|
|
15
|
+
if (range.point) return from;
|
|
16
|
+
if (range.ongoing) return `${from}\u2013${presentLabel}`;
|
|
17
|
+
if (!range.to) return from;
|
|
18
|
+
const to = formatPoint(range.to, options);
|
|
19
|
+
return from === to ? from : `${from}\u2013${to}`;
|
|
20
|
+
}
|
|
21
|
+
function dateSortKey(range) {
|
|
22
|
+
return range?.from.sortKey ?? "";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const TITLE_FIELDS = ["title", "position", "degree", "role", "student"];
|
|
26
|
+
const SECONDARY_FIELDS = ["student", "role", "event", "term", "degree"];
|
|
27
|
+
const LINKED_FIELDS = ["institution", "issuer", "organisation"];
|
|
28
|
+
function entryTitleField(entry, fields = TITLE_FIELDS) {
|
|
29
|
+
return fields.find((field) => {
|
|
30
|
+
const value = entry[field];
|
|
31
|
+
if (typeof value === "string") return value.length > 0;
|
|
32
|
+
return typeof value === "object" && value !== null && "html" in value;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function entrySecondaryParts(entry, titleField) {
|
|
36
|
+
const parts = [];
|
|
37
|
+
for (const field of SECONDARY_FIELDS) {
|
|
38
|
+
if (field === titleField) continue;
|
|
39
|
+
const value = entry[field];
|
|
40
|
+
if (typeof value === "string" && value.length > 0) parts.push({ kind: "text", text: value });
|
|
41
|
+
}
|
|
42
|
+
for (const field of LINKED_FIELDS) {
|
|
43
|
+
const value = entry[field];
|
|
44
|
+
if (value?.label) {
|
|
45
|
+
parts.push({ kind: "linked", linked: value });
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (typeof entry.location === "string" && entry.location.length > 0) {
|
|
50
|
+
parts.push({ kind: "text", text: entry.location });
|
|
51
|
+
}
|
|
52
|
+
return parts;
|
|
53
|
+
}
|
|
54
|
+
function asLinkedList(value) {
|
|
55
|
+
const isLinked = (item) => typeof item === "object" && item !== null && typeof item.label === "string";
|
|
56
|
+
if (Array.isArray(value)) return value.filter(isLinked);
|
|
57
|
+
return isLinked(value) ? [value] : [];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export { LINKED_FIELDS as L, SECONDARY_FIELDS as S, TITLE_FIELDS as T, asLinkedList as a, entryTitleField as b, dateSortKey as d, entrySecondaryParts as e, formatDateRange as f };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
function fieldValue(entry, field) {
|
|
2
|
+
if (field === "year" && !Object.hasOwn(entry, "year")) {
|
|
3
|
+
return entry.date?.from?.year;
|
|
4
|
+
}
|
|
5
|
+
const value = entry[field];
|
|
6
|
+
if (typeof value === "object" && value !== null && "text" in value) return value.text;
|
|
7
|
+
if (typeof value === "object" && value !== null && "label" in value) return value.label;
|
|
8
|
+
return value;
|
|
9
|
+
}
|
|
10
|
+
function matchesWhere(entry, where) {
|
|
11
|
+
return Object.entries(where).every(([field, expected]) => {
|
|
12
|
+
const wanted = (Array.isArray(expected) ? expected : [expected]).map(String);
|
|
13
|
+
return wanted.includes(String(fieldValue(entry, field) ?? ""));
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
function filterWhere(entries, where) {
|
|
17
|
+
return where && Object.keys(where).length ? entries.filter((entry) => matchesWhere(entry, where)) : [...entries];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { filterWhere as a, fieldValue as f, matchesWhere as m };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
const PATH_KEY = "__vitakitPath";
|
|
2
|
+
const isSitePath = (value) => value.startsWith("/") && !value.startsWith("//");
|
|
3
|
+
const sitePathMarker = (path) => ({ [PATH_KEY]: path });
|
|
4
|
+
function isSitePathMarker(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && PATH_KEY in value;
|
|
6
|
+
}
|
|
7
|
+
function resolveSitePaths(value, resolve) {
|
|
8
|
+
if (isSitePathMarker(value)) return resolve(value[PATH_KEY]);
|
|
9
|
+
if (Array.isArray(value)) return value.map((item) => resolveSitePaths(item, resolve));
|
|
10
|
+
if (typeof value === "object" && value !== null) {
|
|
11
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, resolveSitePaths(item, resolve)]));
|
|
12
|
+
}
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
function absoluteSitePath(path, siteUrl) {
|
|
16
|
+
return siteUrl ? `${siteUrl.replace(/\/?$/, "/")}${path.slice(1)}` : path;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export { PATH_KEY as P, absoluteSitePath as a, isSitePath as i, resolveSitePaths as r, sitePathMarker as s };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { pathToFileURL } from 'node:url';
|
|
2
|
+
|
|
3
|
+
class ContentError extends Error {
|
|
4
|
+
constructor(where, message) {
|
|
5
|
+
super(`${where}: ${message}`);
|
|
6
|
+
this.where = where;
|
|
7
|
+
this.name = "ContentError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
function fail(where, message) {
|
|
11
|
+
throw new ContentError(where, message);
|
|
12
|
+
}
|
|
13
|
+
function messageOf(error) {
|
|
14
|
+
if (error instanceof ContentError) return error.message.slice(error.where.length + 2);
|
|
15
|
+
return error instanceof Error ? error.message : String(error);
|
|
16
|
+
}
|
|
17
|
+
function whereOf(error, fallback) {
|
|
18
|
+
return error instanceof ContentError ? error.where : fallback;
|
|
19
|
+
}
|
|
20
|
+
async function importProjectModule(path, mtimeMs, where) {
|
|
21
|
+
try {
|
|
22
|
+
return await import(`${pathToFileURL(path).href}?mtime=${mtimeMs}`);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
fail(where, importFailure(error));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function importFailure(error, node = process.versions.node) {
|
|
28
|
+
if (error?.code === "ERR_UNKNOWN_FILE_EXTENSION") {
|
|
29
|
+
return `cannot load TypeScript on Node ${node}; vitakit needs Node 22.18 or newer`;
|
|
30
|
+
}
|
|
31
|
+
const message = messageOf(error);
|
|
32
|
+
const stack = error instanceof Error ? error.stack ?? "" : "";
|
|
33
|
+
const syntax = /^[^\n]*:(\d+)\n((?:[^\n]*\n)*?[ \t]*\^+)[ \t]*\n/.exec(stack);
|
|
34
|
+
if (syntax) {
|
|
35
|
+
const hint = /^Expected ','/.test(message) ? " (often a comma missing before it)" : "";
|
|
36
|
+
return `line ${syntax[1]}: ${message}${hint}
|
|
37
|
+
${syntax[2].split("\n").map((line2) => ` ${line2}`).join("\n")}`;
|
|
38
|
+
}
|
|
39
|
+
const line = /\.ts(?:\?[^:\s)]*)?:(\d+):\d+/.exec(stack)?.[1];
|
|
40
|
+
return line ? `line ${line}: ${message}` : message;
|
|
41
|
+
}
|
|
42
|
+
function formatZodIssues(error) {
|
|
43
|
+
return error.issues.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
|
|
44
|
+
}
|
|
45
|
+
class Issues {
|
|
46
|
+
constructor(strict) {
|
|
47
|
+
this.strict = strict;
|
|
48
|
+
}
|
|
49
|
+
list = [];
|
|
50
|
+
/** Record a problem, or throw it when strict. */
|
|
51
|
+
record(where, message) {
|
|
52
|
+
if (this.strict) fail(where, message);
|
|
53
|
+
this.list.push({ where, message });
|
|
54
|
+
}
|
|
55
|
+
/** Run `run`; when lenient, record any failure and use `fallback` instead. */
|
|
56
|
+
async attempt(where, run, fallback) {
|
|
57
|
+
try {
|
|
58
|
+
return await run();
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (this.strict) throw error;
|
|
61
|
+
this.list.push({ where: whereOf(error, where), message: messageOf(error) });
|
|
62
|
+
return fallback;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { Issues as I, formatZodIssues as a, fail as f, importProjectModule as i, messageOf as m, whereOf as w };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join, dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
function sourcePath(asset) {
|
|
6
|
+
return asset === "styles" ? asset : join("src", asset);
|
|
7
|
+
}
|
|
8
|
+
let root;
|
|
9
|
+
function packageRoot() {
|
|
10
|
+
if (root) return root;
|
|
11
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
for (let depth = 0; depth < 6; depth += 1) {
|
|
13
|
+
if (existsSync(join(dir, "package.json"))) return root = dir;
|
|
14
|
+
const parent = dirname(dir);
|
|
15
|
+
if (parent === dir) break;
|
|
16
|
+
dir = parent;
|
|
17
|
+
}
|
|
18
|
+
throw new Error("vitakit: cannot locate the package root; the installation is incomplete");
|
|
19
|
+
}
|
|
20
|
+
const built = /[/\\]dist[/\\]/.test(import.meta.url);
|
|
21
|
+
function resolveAsset(asset) {
|
|
22
|
+
const base = packageRoot();
|
|
23
|
+
const order = built ? ["dist", void 0] : [void 0, "dist"];
|
|
24
|
+
const candidates = order.map((prefix) => prefix ? join(base, prefix, asset) : join(base, sourcePath(asset)));
|
|
25
|
+
for (const candidate of candidates) {
|
|
26
|
+
if (existsSync(candidate)) return candidate;
|
|
27
|
+
}
|
|
28
|
+
throw new Error(
|
|
29
|
+
`vitakit: missing bundled asset "${asset}"; looked in
|
|
30
|
+
` + candidates.map((candidate) => ` ${candidate}`).join("\n") + "\nRun `pnpm build` in the package, or reinstall it."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { packageRoot as p, resolveAsset as r };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const REPLACEMENTS = {
|
|
2
|
+
"\\": "\\textbackslash{}",
|
|
3
|
+
"&": "\\&",
|
|
4
|
+
"%": "\\%",
|
|
5
|
+
$: "\\$",
|
|
6
|
+
"#": "\\#",
|
|
7
|
+
_: "\\_",
|
|
8
|
+
"{": "\\{",
|
|
9
|
+
"}": "\\}",
|
|
10
|
+
"~": "\\textasciitilde{}",
|
|
11
|
+
"^": "\\textasciicircum{}"
|
|
12
|
+
};
|
|
13
|
+
const TYPOGRAPHY = [
|
|
14
|
+
// Directional quotes: "..." -> ``...''
|
|
15
|
+
[/"([^"]*)"/g, "``$1''"],
|
|
16
|
+
// An en dash written literally
|
|
17
|
+
[/\u2013/g, "--"],
|
|
18
|
+
[/\u2014/g, "---"],
|
|
19
|
+
[/\u2026/g, "\\ldots{}"],
|
|
20
|
+
[/\u00a0/g, "~"]
|
|
21
|
+
];
|
|
22
|
+
function escapeTex(value, { typography = true, source } = {}) {
|
|
23
|
+
if (source !== void 0 && /(?<!\\)\$/.test(value)) {
|
|
24
|
+
throw new Error(`unbalanced math delimiters in: ${source}`);
|
|
25
|
+
}
|
|
26
|
+
let result = value.replace(/[\\&%$#_{}~^]/g, (character) => REPLACEMENTS[character]);
|
|
27
|
+
if (typography) {
|
|
28
|
+
for (const [pattern, replacement] of TYPOGRAPHY) result = result.replace(pattern, replacement);
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
function escapeProse(prose, source) {
|
|
33
|
+
if (/(?<!\\)\$/.test(prose)) throw new Error(`unbalanced math delimiters in: ${source}`);
|
|
34
|
+
return escapeTex(prose.replace(/\\([!-/:-@[-`{-~])/g, "$1"));
|
|
35
|
+
}
|
|
36
|
+
function escapeRichTex(value) {
|
|
37
|
+
const parts = [];
|
|
38
|
+
const pattern = /(?<!\\)\$\$[\s\S]*?\$\$|(?<!\\)\$(?:\\.|[^$\\])*\$/g;
|
|
39
|
+
let index = 0;
|
|
40
|
+
for (const match of value.matchAll(pattern)) {
|
|
41
|
+
const start = match.index;
|
|
42
|
+
parts.push(escapeProse(value.slice(index, start), value));
|
|
43
|
+
parts.push(match[0]);
|
|
44
|
+
index = start + match[0].length;
|
|
45
|
+
}
|
|
46
|
+
parts.push(escapeProse(value.slice(index), value));
|
|
47
|
+
return parts.join("");
|
|
48
|
+
}
|
|
49
|
+
const URL_SPECIAL = /%[0-9A-Fa-f]{2}|[%\\{}^ #&]/g;
|
|
50
|
+
function escapeUrl(value) {
|
|
51
|
+
return value.replace(URL_SPECIAL, (match) => {
|
|
52
|
+
if (match.length === 3 || match === "#" || match === "&") return `\\${match}`;
|
|
53
|
+
return `\\%${match.charCodeAt(0).toString(16).toUpperCase().padStart(2, "0")}`;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export { escapeTex as a, escapeUrl as b, escapeRichTex as e };
|