@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,135 @@
|
|
|
1
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import matter from 'gray-matter';
|
|
4
|
+
import { parse } from 'yaml';
|
|
5
|
+
import { c as createRenderer, a as prefixRootRelative, l as loadPublicationsSchema, b as loadPublications } from '../chunks/bibliography.mjs';
|
|
6
|
+
import { r as resolveSitePaths, a as absoluteSitePath } from './vitakit.D10HyDhF.mjs';
|
|
7
|
+
import { publicationListNames, LISTS_FILE, loadPublicationLists } from '../chunks/publication-lists.mjs';
|
|
8
|
+
import { findSectionFiles, loadSection, resolveSectionSchema } from '../chunks/sections.mjs';
|
|
9
|
+
import { profile, page } from '../chunks/schemas.mjs';
|
|
10
|
+
import { I as Issues, f as fail, a as formatZodIssues, w as whereOf, m as messageOf } from './vitakit.DCK90zq4.mjs';
|
|
11
|
+
|
|
12
|
+
const FALLBACK_PROFILE = { name: "Your Name", links: [], nav: [], footerHtml: "" };
|
|
13
|
+
async function loadYaml(file, where) {
|
|
14
|
+
const raw = await readFile(file, "utf8").catch(() => fail(where, "missing"));
|
|
15
|
+
try {
|
|
16
|
+
return parse(raw);
|
|
17
|
+
} catch (error) {
|
|
18
|
+
fail(where, messageOf(error));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function loadProfile(contentDir, baseURL, renderer, target) {
|
|
22
|
+
const data = await loadYaml(join(contentDir, "profile.yml"), "content/profile.yml");
|
|
23
|
+
const parsed = profile.safeParse(data);
|
|
24
|
+
if (!parsed.success) fail("content/profile.yml", `invalid
|
|
25
|
+
${formatZodIssues(parsed.error)}`);
|
|
26
|
+
const prefix = baseURL.replace(/\/$/, "");
|
|
27
|
+
const { avatar, nav, footer, ...rest } = parsed.data;
|
|
28
|
+
return resolveSitePaths({
|
|
29
|
+
...rest,
|
|
30
|
+
...avatar ? { avatar: prefixRootRelative(avatar, prefix) } : {},
|
|
31
|
+
nav: nav.map((item) => ({ label: item.label, url: item.to })),
|
|
32
|
+
footerHtml: footer ? renderer.inline(footer) : ""
|
|
33
|
+
}, sitePathResolver(target, baseURL, parsed.data.url));
|
|
34
|
+
}
|
|
35
|
+
function sitePathResolver(target, baseURL, siteUrl) {
|
|
36
|
+
const prefix = baseURL.replace(/\/$/, "");
|
|
37
|
+
return target === "pdf" ? (path) => absoluteSitePath(path, siteUrl) : (path) => prefixRootRelative(path, prefix);
|
|
38
|
+
}
|
|
39
|
+
function errorPage(route, where, message) {
|
|
40
|
+
const escaped = message.replace(/[&<>]/g, (character) => ({ "&": "&", "<": "<", ">": ">" })[character]);
|
|
41
|
+
return {
|
|
42
|
+
route,
|
|
43
|
+
title: `Error in ${where}`,
|
|
44
|
+
titleHtml: `Error in <code>${where}</code>`,
|
|
45
|
+
order: -1,
|
|
46
|
+
bodyHtml: `<pre>${escaped}</pre>`,
|
|
47
|
+
error: message
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
async function loadPage(dir, file, renderer) {
|
|
51
|
+
const where = `content/pages/${file}`;
|
|
52
|
+
const slug = file.slice(0, -".md".length);
|
|
53
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug)) {
|
|
54
|
+
fail(where, "file name must be lowercase kebab-case, e.g. `research-interests.md`");
|
|
55
|
+
}
|
|
56
|
+
const { data, content } = matter(await readFile(join(dir, file), "utf8"));
|
|
57
|
+
const parsed = page.safeParse(data);
|
|
58
|
+
if (!parsed.success) fail(where, `invalid front matter
|
|
59
|
+
${formatZodIssues(parsed.error)}`);
|
|
60
|
+
const route = parsed.data.route ?? (slug === "index" ? "/" : `/${slug}/`);
|
|
61
|
+
try {
|
|
62
|
+
return {
|
|
63
|
+
route,
|
|
64
|
+
title: parsed.data.title,
|
|
65
|
+
titleHtml: renderer.inline(parsed.data.title),
|
|
66
|
+
description: parsed.data.description,
|
|
67
|
+
order: parsed.data.order,
|
|
68
|
+
bodyHtml: content.trim() ? renderer.markdown(content) : ""
|
|
69
|
+
};
|
|
70
|
+
} catch (error) {
|
|
71
|
+
fail(where, messageOf(error));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async function loadPages(contentDir, renderer, issues) {
|
|
75
|
+
const dir = join(contentDir, "pages");
|
|
76
|
+
const files = (await readdir(dir).catch(() => [])).filter((name) => name.endsWith(".md")).sort();
|
|
77
|
+
const pages = await Promise.all(files.map(async (file) => {
|
|
78
|
+
try {
|
|
79
|
+
return await loadPage(dir, file, renderer);
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (issues.strict) throw error;
|
|
82
|
+
const where = whereOf(error, `content/pages/${file}`);
|
|
83
|
+
const message = messageOf(error);
|
|
84
|
+
issues.record(where, message);
|
|
85
|
+
const slug = file.slice(0, -".md".length);
|
|
86
|
+
return errorPage(slug === "index" ? "/" : `/${slug}/`, where, message);
|
|
87
|
+
}
|
|
88
|
+
}));
|
|
89
|
+
const seen = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const [index, page] of pages.entries()) {
|
|
91
|
+
const previous = seen.get(page.route);
|
|
92
|
+
if (previous) issues.record(`content/pages/${files[index]}`, `route ${page.route} is already used by ${previous}`);
|
|
93
|
+
else seen.set(page.route, `content/pages/${files[index]}`);
|
|
94
|
+
}
|
|
95
|
+
return pages.sort((a, b) => a.order - b.order || a.route.localeCompare(b.route));
|
|
96
|
+
}
|
|
97
|
+
async function loadSections(rootDir, maxVisibility, renderer, resolvePath, issues) {
|
|
98
|
+
const { files, problems } = await issues.attempt("content/", () => findSectionFiles(rootDir), { files: [], problems: [] });
|
|
99
|
+
for (const problem of problems) issues.record(problem.where, problem.message);
|
|
100
|
+
const sections = {};
|
|
101
|
+
for (const file of files) {
|
|
102
|
+
sections[file.id] = await issues.attempt(
|
|
103
|
+
file.where,
|
|
104
|
+
async () => loadSection(await resolveSectionSchema(rootDir, file), { maxVisibility, renderer, resolvePath }),
|
|
105
|
+
[]
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
return sections;
|
|
109
|
+
}
|
|
110
|
+
async function loadPublicationContent(rootDir, renderer, baseURL, issues) {
|
|
111
|
+
const empty = (names) => Object.fromEntries(names.map((name) => [name, { count: 0, groups: [] }]));
|
|
112
|
+
const schema = await issues.attempt("schemas/publications.ts", () => loadPublicationsSchema(rootDir), void 0);
|
|
113
|
+
if (!schema) return empty(await publicationListNames(rootDir));
|
|
114
|
+
const lists = await issues.attempt(LISTS_FILE, () => loadPublicationLists(rootDir, schema), {});
|
|
115
|
+
return issues.attempt("content/publications.bib", () => loadPublications(join(rootDir, "content", "publications.bib"), renderer, schema, lists, baseURL, issues), empty(Object.keys(lists)));
|
|
116
|
+
}
|
|
117
|
+
async function loadContent(rootDir, options = {}) {
|
|
118
|
+
const strict = options.strict ?? true;
|
|
119
|
+
const baseURL = options.baseURL ?? "/";
|
|
120
|
+
const maxVisibility = options.maxVisibility ?? "public";
|
|
121
|
+
const issues = new Issues(strict);
|
|
122
|
+
const renderer = createRenderer({ baseURL });
|
|
123
|
+
const contentDir = join(rootDir, "content");
|
|
124
|
+
const target = options.target ?? "website";
|
|
125
|
+
const profile = await issues.attempt("content/profile.yml", () => loadProfile(contentDir, baseURL, renderer, target), FALLBACK_PROFILE);
|
|
126
|
+
const resolvePath = sitePathResolver(target, baseURL, profile.url);
|
|
127
|
+
const [pages, sections, publications] = await Promise.all([
|
|
128
|
+
loadPages(contentDir, renderer, issues),
|
|
129
|
+
loadSections(rootDir, maxVisibility, renderer, resolvePath, issues),
|
|
130
|
+
loadPublicationContent(rootDir, renderer, baseURL, issues)
|
|
131
|
+
]);
|
|
132
|
+
return { profile, pages, sections, publications, issues: issues.list };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { loadContent as l };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { f as fail } from './vitakit.DCK90zq4.mjs';
|
|
3
|
+
import { i as isSitePath, s as sitePathMarker, P as PATH_KEY } from './vitakit.D10HyDhF.mjs';
|
|
4
|
+
|
|
5
|
+
const MARKER_KEY = "__vitakitRich";
|
|
6
|
+
function isRichMarker(value) {
|
|
7
|
+
return typeof value === "object" && value !== null && MARKER_KEY in value;
|
|
8
|
+
}
|
|
9
|
+
function richMarker(value) {
|
|
10
|
+
return { [MARKER_KEY]: value };
|
|
11
|
+
}
|
|
12
|
+
function renderRichFields(value, renderer, path, where) {
|
|
13
|
+
if (isRichMarker(value)) {
|
|
14
|
+
try {
|
|
15
|
+
return { text: value[MARKER_KEY], html: renderer.inline(value[MARKER_KEY]) };
|
|
16
|
+
} catch (error) {
|
|
17
|
+
fail(where, `${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
if (Array.isArray(value)) {
|
|
21
|
+
return value.map((item, index) => renderRichFields(item, renderer, `${path}[${index}]`, where));
|
|
22
|
+
}
|
|
23
|
+
if (typeof value === "object" && value !== null) {
|
|
24
|
+
return Object.fromEntries(Object.entries(value).map(
|
|
25
|
+
([key, item]) => [key, renderRichFields(item, renderer, path ? `${path}.${key}` : key, where)]
|
|
26
|
+
));
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const text = z.string().trim().min(1);
|
|
32
|
+
const richText = text.transform((value) => richMarker(value));
|
|
33
|
+
const absoluteUrl = z.string().trim().refine(
|
|
34
|
+
// One check, not `.url().refine(...)`: zod runs a refinement even after
|
|
35
|
+
// `.url()` failed, and `new URL()` then throws instead of reporting.
|
|
36
|
+
(value) => URL.canParse(value) && ["http:", "https:", "mailto:"].includes(new URL(value).protocol),
|
|
37
|
+
"expected an http(s) or mailto URL"
|
|
38
|
+
);
|
|
39
|
+
const url = z.string().trim().transform((value, context) => {
|
|
40
|
+
if (isSitePath(value) && !/\s/.test(value)) return sitePathMarker(value);
|
|
41
|
+
const parsed = absoluteUrl.safeParse(value);
|
|
42
|
+
if (parsed.success) return parsed.data;
|
|
43
|
+
context.addIssue({ code: "custom", message: "expected an http(s) or mailto URL, or a path on the site such as /slides/talk.pdf" });
|
|
44
|
+
return z.NEVER;
|
|
45
|
+
});
|
|
46
|
+
const DATE = /^(\d{4})(?:-(\d{2}))?(?:-(\d{2}))?$/;
|
|
47
|
+
function parseDatePoint(value, context) {
|
|
48
|
+
const match = DATE.exec(value);
|
|
49
|
+
if (!match) {
|
|
50
|
+
context.addIssue({ code: "custom", message: `expected a date like 2024, 2024-03 or 2024-03-15, got "${value}"` });
|
|
51
|
+
return void 0;
|
|
52
|
+
}
|
|
53
|
+
const [, y, m, d] = match;
|
|
54
|
+
const year = Number(y);
|
|
55
|
+
const month = m === void 0 ? void 0 : Number(m);
|
|
56
|
+
const day = d === void 0 ? void 0 : Number(d);
|
|
57
|
+
if (month !== void 0 && (month < 1 || month > 12)) {
|
|
58
|
+
context.addIssue({ code: "custom", message: `month out of range in "${value}"` });
|
|
59
|
+
return void 0;
|
|
60
|
+
}
|
|
61
|
+
if (day !== void 0) {
|
|
62
|
+
const date = new Date(Date.UTC(year, month - 1, day));
|
|
63
|
+
if (date.getUTCMonth() !== month - 1 || date.getUTCDate() !== day) {
|
|
64
|
+
context.addIssue({ code: "custom", message: `"${value}" is not a real date` });
|
|
65
|
+
return void 0;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
raw: value,
|
|
70
|
+
year,
|
|
71
|
+
month,
|
|
72
|
+
day,
|
|
73
|
+
// Pad missing parts so that 2024 < 2024-03 < 2024-03-15 sorts correctly.
|
|
74
|
+
sortKey: `${String(year).padStart(4, "0")}-${String(month ?? 1).padStart(2, "0")}-${String(day ?? 1).padStart(2, "0")}`
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
const datePoint = z.union([z.string(), z.number().int()]).transform((value) => String(value));
|
|
78
|
+
const dateRange = z.union([
|
|
79
|
+
datePoint,
|
|
80
|
+
z.strictObject({
|
|
81
|
+
from: datePoint,
|
|
82
|
+
to: z.union([datePoint, z.literal("present")]).optional()
|
|
83
|
+
})
|
|
84
|
+
]).transform((value, context) => {
|
|
85
|
+
const source = typeof value === "string" ? { from: value, to: void 0 } : value;
|
|
86
|
+
const from = parseDatePoint(source.from, context);
|
|
87
|
+
const ongoing = source.to === "present";
|
|
88
|
+
const to = source.to === void 0 || ongoing ? void 0 : parseDatePoint(source.to, context);
|
|
89
|
+
if (!from || source.to !== void 0 && !ongoing && !to) return z.NEVER;
|
|
90
|
+
if (to && to.sortKey < from.sortKey) {
|
|
91
|
+
context.addIssue({ code: "custom", message: `range ends (${to.raw}) before it starts (${from.raw})` });
|
|
92
|
+
return z.NEVER;
|
|
93
|
+
}
|
|
94
|
+
return { from, to, ongoing, point: typeof value === "string" };
|
|
95
|
+
});
|
|
96
|
+
function urlLabel(value) {
|
|
97
|
+
return typeof value === "string" ? value.replace(/^(?:https?:\/\/|mailto:)/, "").replace(/\/$/, "") : value[PATH_KEY];
|
|
98
|
+
}
|
|
99
|
+
const linked = z.union([
|
|
100
|
+
url.transform((value) => ({ label: urlLabel(value), url: value })),
|
|
101
|
+
text.transform((label) => ({ label })),
|
|
102
|
+
z.strictObject({ label: text, url: url.optional() }),
|
|
103
|
+
z.strictObject({ name: text, url: url.optional() }).transform(({ name, url: url2 }) => ({ label: name, url: url2 }))
|
|
104
|
+
]).transform((value) => {
|
|
105
|
+
const target = "url" in value ? value.url : void 0;
|
|
106
|
+
return target === void 0 ? { label: value.label } : { label: value.label, url: target };
|
|
107
|
+
});
|
|
108
|
+
const requiredLink = linked.transform((value, context) => {
|
|
109
|
+
if (value.url !== void 0) return value;
|
|
110
|
+
context.addIssue({ code: "custom", message: "expected a URL, or { label, url }" });
|
|
111
|
+
return z.NEVER;
|
|
112
|
+
});
|
|
113
|
+
const visibility = z.enum(["public", "cv", "private"]).default("public");
|
|
114
|
+
function entry(shape) {
|
|
115
|
+
return z.strictObject({ visibility, ...shape });
|
|
116
|
+
}
|
|
117
|
+
function section(shape) {
|
|
118
|
+
return z.array(entry(shape));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export { MARKER_KEY as M, richText as a, absoluteUrl as b, renderRichFields as c, dateRange as d, entry as e, linked as l, requiredLink as r, section as s, text as t, url as u, visibility as v };
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { join, relative, sep } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const posix = (path) => path.split(sep).join("/");
|
|
5
|
+
async function vueFiles(dir) {
|
|
6
|
+
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
7
|
+
const files = [];
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
const path = join(dir, entry.name);
|
|
10
|
+
if (entry.isDirectory()) files.push(...await vueFiles(path));
|
|
11
|
+
else if (entry.name.endsWith(".vue")) files.push(path);
|
|
12
|
+
}
|
|
13
|
+
return files.sort();
|
|
14
|
+
}
|
|
15
|
+
const pascal = (name) => name.split(/[-_.\s]+/).filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join("");
|
|
16
|
+
function componentName(pathBelowComponents) {
|
|
17
|
+
const parts = posix(pathBelowComponents).replace(/\.vue$/, "").split("/").map(pascal);
|
|
18
|
+
let name = "";
|
|
19
|
+
for (const part of parts) name = part.startsWith(name) ? part : name + part;
|
|
20
|
+
return name;
|
|
21
|
+
}
|
|
22
|
+
function pageRoute(pathBelowPages) {
|
|
23
|
+
const path = posix(pathBelowPages).replace(/\.vue$/, "").replace(/(^|\/)index$/, "");
|
|
24
|
+
return path ? `/${path}/` : "/";
|
|
25
|
+
}
|
|
26
|
+
function literal(expression) {
|
|
27
|
+
return /^\s*(['"`])([^'"`$]*)\1\s*$/.exec(expression)?.[2];
|
|
28
|
+
}
|
|
29
|
+
const ELEMENT = 1;
|
|
30
|
+
const ATTRIBUTE = 6;
|
|
31
|
+
function attribute(node, name) {
|
|
32
|
+
for (const prop of node.props ?? []) {
|
|
33
|
+
if (prop.type === ATTRIBUTE && prop.name === name) return { static: prop.value?.content ?? "" };
|
|
34
|
+
if (prop.type !== ATTRIBUTE && prop.name === "bind" && prop.arg?.content === name) {
|
|
35
|
+
return { expression: prop.exp?.content ?? "" };
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
function lineOf(text, offset) {
|
|
41
|
+
let line = 1;
|
|
42
|
+
for (let index = 0; index < offset; index++) if (text.charCodeAt(index) === 10) line++;
|
|
43
|
+
return line;
|
|
44
|
+
}
|
|
45
|
+
async function findWebPlacements(rootDir) {
|
|
46
|
+
const { parse } = await import('@vue/compiler-sfc');
|
|
47
|
+
const appDir = join(rootDir, "app");
|
|
48
|
+
const componentsDir = join(appDir, "components");
|
|
49
|
+
const pagesDir = join(appDir, "pages");
|
|
50
|
+
const components = /* @__PURE__ */ new Map();
|
|
51
|
+
for (const file of await vueFiles(componentsDir)) {
|
|
52
|
+
components.set(componentName(relative(componentsDir, file)), posix(relative(rootDir, file)));
|
|
53
|
+
}
|
|
54
|
+
const placements = [];
|
|
55
|
+
const publications = [];
|
|
56
|
+
const dynamic = [];
|
|
57
|
+
for (const path of await vueFiles(appDir)) {
|
|
58
|
+
const file = posix(relative(rootDir, path));
|
|
59
|
+
const inPages = !relative(pagesDir, path).startsWith("..");
|
|
60
|
+
const inComponents = !relative(componentsDir, path).startsWith("..");
|
|
61
|
+
const route = inPages ? pageRoute(relative(pagesDir, path)) : void 0;
|
|
62
|
+
const source = await readFile(path, "utf8");
|
|
63
|
+
const { descriptor } = parse(source, { filename: path });
|
|
64
|
+
const visit = (node) => {
|
|
65
|
+
if (node.type === ELEMENT && node.tag) {
|
|
66
|
+
const tag = node.tag.includes("-") ? pascal(node.tag) : node.tag;
|
|
67
|
+
if (tag === "VitaPublications") {
|
|
68
|
+
const list = attribute(node, "list");
|
|
69
|
+
const title = attribute(node, "title");
|
|
70
|
+
const name = list?.static ?? (list?.expression !== void 0 ? literal(list.expression) : void 0);
|
|
71
|
+
publications.push({
|
|
72
|
+
file,
|
|
73
|
+
route,
|
|
74
|
+
line: node.loc.start.line,
|
|
75
|
+
...name !== void 0 ? { list: name } : list?.expression !== void 0 ? { expression: list.expression } : {},
|
|
76
|
+
...title?.static !== void 0 ? { title: title.static } : {},
|
|
77
|
+
selecting: SELECTING_PROPS.filter((prop) => attribute(node, prop) !== void 0)
|
|
78
|
+
});
|
|
79
|
+
} else if (tag === "VitaSection" || components.has(tag)) {
|
|
80
|
+
const id = attribute(node, "id");
|
|
81
|
+
const section = id?.static ?? (id?.expression !== void 0 ? literal(id.expression) : void 0);
|
|
82
|
+
if (section) {
|
|
83
|
+
const title = attribute(node, "title");
|
|
84
|
+
const where = attribute(node, "where");
|
|
85
|
+
const limit = attribute(node, "limit");
|
|
86
|
+
placements.push({
|
|
87
|
+
section,
|
|
88
|
+
file,
|
|
89
|
+
route,
|
|
90
|
+
line: node.loc.start.line,
|
|
91
|
+
component: tag,
|
|
92
|
+
...title?.static !== void 0 ? { title: title.static } : {},
|
|
93
|
+
...where?.expression !== void 0 ? { where: where.expression } : {},
|
|
94
|
+
...limit ? { limit: limit.static ?? limit.expression } : {}
|
|
95
|
+
});
|
|
96
|
+
} else if (id?.expression !== void 0 && !inComponents) {
|
|
97
|
+
dynamic.push({ file, line: node.loc.start.line, usage: `<${node.tag} :id="${id.expression}">` });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
for (const child of node.children ?? []) visit(child);
|
|
102
|
+
};
|
|
103
|
+
if (descriptor.template?.ast) visit(descriptor.template.ast);
|
|
104
|
+
for (const block of [descriptor.script, descriptor.scriptSetup]) {
|
|
105
|
+
if (!block) continue;
|
|
106
|
+
const start = block.loc.start.line - 1;
|
|
107
|
+
for (const match of block.content.matchAll(/\buseSection\(\s*([^)]*?)\s*\)/g)) {
|
|
108
|
+
const line = start + lineOf(block.content, match.index);
|
|
109
|
+
const section = literal(match[1]);
|
|
110
|
+
if (section) placements.push({ section, file, route, line, component: "code" });
|
|
111
|
+
else if (!inComponents) dynamic.push({ file, line, usage: `useSection(${match[1]})` });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { placements, publications, dynamic, components };
|
|
116
|
+
}
|
|
117
|
+
const SELECTING_PROPS = ["groups", "headings", "where", "limit", "sort", "groupBy", "group-by"];
|
|
118
|
+
function publicationPlacementProblems(web, pdf, lists) {
|
|
119
|
+
const known = `the lists are ${lists.join(", ") || "(none)"} (content/publication-lists.yml)`;
|
|
120
|
+
const unknown = (name) => `no publication list "${name}"; ${known}`;
|
|
121
|
+
const migrate = "`vitakit migrate` converts it";
|
|
122
|
+
const problems = [];
|
|
123
|
+
for (const placement of web) {
|
|
124
|
+
const where = `${placement.file}:${placement.line}`;
|
|
125
|
+
const legacy = placement.selecting.filter((prop) => prop === "groups" || prop === "headings");
|
|
126
|
+
if (placement.selecting.length) {
|
|
127
|
+
problems.push({
|
|
128
|
+
where,
|
|
129
|
+
message: `VitaPublications does not take ${placement.selecting.join(", ")}: which publications a list shows is set in content/publication-lists.yml${legacy.length ? `; ${migrate}` : ""}`
|
|
130
|
+
});
|
|
131
|
+
} else if (placement.list === void 0 && placement.expression === void 0) {
|
|
132
|
+
problems.push({ where, message: `VitaPublications needs a list, e.g. <VitaPublications list="all" />; ${known}` });
|
|
133
|
+
} else if (placement.list !== void 0 && !lists.includes(placement.list)) {
|
|
134
|
+
problems.push({ where, message: unknown(placement.list) });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const placement of pdf) {
|
|
138
|
+
const where = `${placement.file}:${placement.line}`;
|
|
139
|
+
if ("list" in placement) {
|
|
140
|
+
if (!lists.includes(placement.list)) problems.push({ where, message: unknown(placement.list) });
|
|
141
|
+
} else if (placement.macro === "vitainput" && placement.section === "publications") {
|
|
142
|
+
problems.push({ where, message: `\\vitainput{publications} is now \\vitapublications[Heading]{list}, with a list from content/publication-lists.yml; ${migrate}` });
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return problems;
|
|
146
|
+
}
|
|
147
|
+
function stripComment(line) {
|
|
148
|
+
const match = /(^|[^\\])%/.exec(line);
|
|
149
|
+
return match ? line.slice(0, match.index + match[1].length) : line;
|
|
150
|
+
}
|
|
151
|
+
function findPdfPlacements(document, file, source) {
|
|
152
|
+
const placements = [];
|
|
153
|
+
const lines = source.split("\n");
|
|
154
|
+
let inBody = false;
|
|
155
|
+
for (const [index, raw] of lines.entries()) {
|
|
156
|
+
const line = stripComment(raw);
|
|
157
|
+
if (!inBody) {
|
|
158
|
+
if (/\\begin\{document\}/.test(line)) inBody = true;
|
|
159
|
+
else continue;
|
|
160
|
+
}
|
|
161
|
+
for (const match of line.matchAll(/\\vitasection(?:\[([^\]]*)\])?\{([^}]*)\}/g)) {
|
|
162
|
+
placements.push({
|
|
163
|
+
section: match[2].trim(),
|
|
164
|
+
document,
|
|
165
|
+
file,
|
|
166
|
+
line: index + 1,
|
|
167
|
+
macro: "vitasection",
|
|
168
|
+
...match[1] !== void 0 ? { heading: match[1] } : {}
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
for (const match of line.matchAll(/\\vitainput\{([^}]*)\}/g)) {
|
|
172
|
+
placements.push({ section: match[1].trim(), document, file, line: index + 1, macro: "vitainput" });
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return placements;
|
|
176
|
+
}
|
|
177
|
+
function findPdfPublicationPlacements(document, file, source) {
|
|
178
|
+
const placements = [];
|
|
179
|
+
let inBody = false;
|
|
180
|
+
for (const [index, raw] of source.split("\n").entries()) {
|
|
181
|
+
const line = stripComment(raw);
|
|
182
|
+
if (!inBody) {
|
|
183
|
+
if (/\\begin\{document\}/.test(line)) inBody = true;
|
|
184
|
+
else continue;
|
|
185
|
+
}
|
|
186
|
+
for (const match of line.matchAll(/\\vitapublications(?:\[([^\]]*)\])?\{([^}]*)\}/g)) {
|
|
187
|
+
placements.push({ list: match[2].trim(), document, file, line: index + 1, ...match[1] !== void 0 ? { heading: match[1] } : {} });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return placements;
|
|
191
|
+
}
|
|
192
|
+
function documentDirective(source) {
|
|
193
|
+
for (const [index, raw] of source.split("\n").entries()) {
|
|
194
|
+
const match = /\\vitadocument\{([^}]*)\}/.exec(stripComment(raw));
|
|
195
|
+
if (match) return { name: match[1].trim(), line: index + 1 };
|
|
196
|
+
}
|
|
197
|
+
return void 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export { pageRoute as a, findWebPlacements as b, findPdfPublicationPlacements as c, documentDirective as d, findPdfPlacements as f, publicationPlacementProblems as p };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function lineStart(source, offset) {
|
|
2
|
+
return source.lastIndexOf("\n", offset - 1) + 1;
|
|
3
|
+
}
|
|
4
|
+
function lineEnd(source, offset) {
|
|
5
|
+
const end = source.indexOf("\n", offset);
|
|
6
|
+
return end === -1 ? source.length : end;
|
|
7
|
+
}
|
|
8
|
+
function indentationAt(source, offset) {
|
|
9
|
+
return /^[ \t]*/.exec(source.slice(lineStart(source, offset)))[0];
|
|
10
|
+
}
|
|
11
|
+
function insertLinesAfter(source, offset, lines) {
|
|
12
|
+
const end = lineEnd(source, offset);
|
|
13
|
+
return `${source.slice(0, end)}
|
|
14
|
+
${lines.join("\n")}${source.slice(end)}`;
|
|
15
|
+
}
|
|
16
|
+
function insertLinesBefore(source, offset, lines) {
|
|
17
|
+
const start = lineStart(source, offset);
|
|
18
|
+
return `${source.slice(0, start)}${lines.join("\n")}
|
|
19
|
+
${source.slice(start)}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { lineEnd as a, insertLinesAfter as b, insertLinesBefore as c, indentationAt as i, lineStart as l };
|