@anja.codes/vitakit 0.2.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.
Files changed (40) hide show
  1. package/README.md +4 -3
  2. package/dist/check.mjs +14 -12
  3. package/dist/chunks/authoring.mjs +50 -45
  4. package/dist/{shared/vitakit.By5VWQ1n.mjs → chunks/bibliography.mjs} +73 -170
  5. package/dist/chunks/build.mjs +19 -17
  6. package/dist/chunks/documents.mjs +15 -6
  7. package/dist/chunks/draft.mjs +3 -3
  8. package/dist/chunks/eject.mjs +2 -2
  9. package/dist/chunks/emit.mjs +38 -22
  10. package/dist/chunks/fields.mjs +2 -2
  11. package/dist/chunks/inventory.mjs +29 -27
  12. package/dist/chunks/migrations.mjs +218 -0
  13. package/dist/chunks/plan.mjs +36 -8
  14. package/dist/chunks/publication-lists.mjs +202 -0
  15. package/dist/chunks/schemas.mjs +2 -2
  16. package/dist/chunks/sections.mjs +3 -3
  17. package/dist/chunks/validate.mjs +25 -16
  18. package/dist/chunks/vue.mjs +89 -52
  19. package/dist/chunks/yaml.mjs +62 -0
  20. package/dist/cli.mjs +1 -1
  21. package/dist/index.d.mts +9 -9
  22. package/dist/index.mjs +12 -10
  23. package/dist/nuxt.mjs +19 -11
  24. package/dist/runtime/components/VitaPublications.vue +21 -22
  25. package/dist/runtime/composables/useVitakit.ts +14 -3
  26. package/dist/schemas/publications.ts +9 -7
  27. package/dist/shared/index.d.mts +2 -2
  28. package/dist/shared/{vitakit.BfpxDtz1.d.mts → vitakit.BfR_mUHm.d.mts} +17 -5
  29. package/dist/shared/{vitakit.BZZIsWlY.mjs → vitakit.CCTDg7pz.mjs} +21 -1
  30. package/dist/shared/{vitakit.DbSeENVS.mjs → vitakit.DCK90zq4.mjs} +10 -1
  31. package/dist/shared/vitakit.DteA9vLy.mjs +135 -0
  32. package/dist/shared/{vitakit.BerUaxtF.mjs → vitakit.DvABWuUc.mjs} +1 -1
  33. package/dist/shared/{vitakit.Do5HC_MD.mjs → vitakit.DvvUYUTS.mjs} +61 -3
  34. package/dist/shared/vitakit.UfoXdL7U.mjs +22 -0
  35. package/dist/shared/{vitakit.CoWX2Z4R.mjs → vitakit.zwr1ssO5.mjs} +353 -57
  36. package/dist/tex/templates/publications.eta +4 -2
  37. package/dist/tex/vitakit-cv.cls +15 -4
  38. package/dist/tex.d.mts +29 -8
  39. package/dist/tex.mjs +13 -10
  40. package/package.json +1 -1
@@ -0,0 +1,202 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { parse } from 'yaml';
4
+ import { z } from 'zod';
5
+ import { f as fail, m as messageOf, a as formatZodIssues } from '../shared/vitakit.DCK90zq4.mjs';
6
+ import 'node:url';
7
+
8
+ const LISTS_FILE = "content/publication-lists.yml";
9
+ const LISTS_HEADER = `# Publication lists: which publications of publications.bib a list shows,
10
+ # in what order and in which groups. A page shows one with
11
+ # <VitaPublications list="recent" title="Recent publications" />
12
+ # and a PDF with
13
+ # \\vitapublications[Recent publications]{recent}
14
+ #
15
+ # Settings, all optional:
16
+ # where: { status: preprint }, { keywords: selected }, { year: 2024 } --
17
+ # fields of the entries in publications.bib; [a, b] means a or b
18
+ # keys: [doe2024, roe2023] -- exactly these, in this order
19
+ # since, until: years
20
+ # sort: date (newest first), year, title, source (as in publications.bib),
21
+ # or a field; order: asc or desc
22
+ # limit: at most this many
23
+ # groupBy: status, year or a field; otherLabel: the heading for the entries
24
+ # without it (Other)
25
+ # \`vitakit explain publications\` shows what each list contains and where.
26
+ `;
27
+ const DERIVED = /* @__PURE__ */ new Set(["key", "status", "year"]);
28
+ const NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
29
+ const field = z.string().regex(/^[A-Za-z][\w-]*$/, "expected a BibTeX field name").transform((name) => name.toLowerCase());
30
+ const scalar = z.union([z.string(), z.number(), z.boolean()]);
31
+ const listInput = z.strictObject({
32
+ where: z.record(z.string(), z.union([scalar, z.array(scalar).min(1)])).default({}),
33
+ keys: z.array(z.string().min(1)).min(1).optional(),
34
+ since: z.number().int().optional(),
35
+ until: z.number().int().optional(),
36
+ sort: field.optional(),
37
+ order: z.enum(["asc", "desc"]).optional(),
38
+ limit: z.number().int().positive().optional(),
39
+ groupBy: field.optional(),
40
+ otherLabel: z.string().trim().min(1).optional()
41
+ });
42
+ function resolveList(name, value, schema) {
43
+ const at = (option) => `${name}.${option}`;
44
+ if (!NAME.test(name)) fail(LISTS_FILE, `${name}: a list name is lowercase kebab-case, e.g. recent-papers`);
45
+ const parsed = listInput.safeParse(value ?? {});
46
+ if (!parsed.success) fail(LISTS_FILE, `${name}: invalid
47
+ ${formatZodIssues(parsed.error)}`);
48
+ const input = parsed.data;
49
+ const where = {};
50
+ for (const [key, wanted] of Object.entries(input.where)) {
51
+ const parsedField = field.safeParse(key);
52
+ if (!parsedField.success) fail(LISTS_FILE, `${at(`where.${key}`)}: expected a BibTeX field name`);
53
+ where[parsedField.data] = wanted;
54
+ }
55
+ const statuses = schema.statuses.map((status) => status.id);
56
+ for (const status of where.status === void 0 ? [] : [where.status].flat()) {
57
+ if (!statuses.includes(String(status).toLowerCase())) {
58
+ fail(LISTS_FILE, `${at("where.status")}: no status "${status}"; the statuses are ${statuses.join(", ")} (schemas/publications.ts)`);
59
+ }
60
+ }
61
+ if (input.keys && input.sort) fail(LISTS_FILE, `${at("sort")}: a list of keys is shown in the order of its keys; remove sort or keys`);
62
+ if (input.order && (input.keys || input.sort === "source")) fail(LISTS_FILE, `${at("order")}: only applies when sorting by a field`);
63
+ if (input.groupBy === "keywords") fail(LISTS_FILE, `${at("groupBy")}: an entry can have several keywords; group by another field`);
64
+ if (input.otherLabel && (!input.groupBy || input.groupBy === "status")) {
65
+ fail(LISTS_FILE, `${at("otherLabel")}: only applies when grouping by year or by a field`);
66
+ }
67
+ const sort = input.sort ?? "date";
68
+ return {
69
+ where,
70
+ ...input.keys ? { keys: input.keys } : {},
71
+ ...input.since !== void 0 ? { since: input.since } : {},
72
+ ...input.until !== void 0 ? { until: input.until } : {},
73
+ sort,
74
+ order: input.order ?? (sort === "date" || sort === "year" ? "desc" : "asc"),
75
+ ...input.limit !== void 0 ? { limit: input.limit } : {},
76
+ ...input.groupBy ? { groupBy: input.groupBy } : {},
77
+ otherLabel: input.otherLabel ?? "Other"
78
+ };
79
+ }
80
+ async function publicationListNames(rootDir) {
81
+ try {
82
+ const data = parse(await readFile(join(rootDir, LISTS_FILE), "utf8"));
83
+ return data && typeof data === "object" && !Array.isArray(data) ? Object.keys(data) : [];
84
+ } catch {
85
+ return [];
86
+ }
87
+ }
88
+ async function loadPublicationLists(rootDir, schema) {
89
+ const raw = await readFile(join(rootDir, LISTS_FILE), "utf8").catch((error) => {
90
+ if (error.code === "ENOENT") return void 0;
91
+ throw error;
92
+ });
93
+ if (raw === void 0) return {};
94
+ let data;
95
+ try {
96
+ data = parse(raw);
97
+ } catch (error) {
98
+ fail(LISTS_FILE, messageOf(error));
99
+ }
100
+ if (data === null || data === void 0) return {};
101
+ if (typeof data !== "object" || Array.isArray(data)) fail(LISTS_FILE, "expected one list per name, e.g. `recent: { limit: 3 }`");
102
+ return Object.fromEntries(Object.entries(data).map(([name, value]) => [name, resolveList(name, value, schema)]));
103
+ }
104
+ function publicationStatus(entry, schema) {
105
+ return (entry.custom.status?.trim() || schema.defaultStatus).toLowerCase();
106
+ }
107
+ const plain = (value) => {
108
+ const text = typeof value === "string" ? value.replace(/[{}]/g, "").trim() : void 0;
109
+ return text || void 0;
110
+ };
111
+ function valueOf(entry, name, schema) {
112
+ if (name === "key") return entry.key;
113
+ if (name === "status") return publicationStatus(entry, schema);
114
+ if (name === "year") return entry.year === null ? void 0 : String(entry.year);
115
+ return plain(entry.raw[name]);
116
+ }
117
+ function matches(entry, name, wanted, schema) {
118
+ const value = valueOf(entry, name, schema);
119
+ const actual = name === "keywords" ? value?.split(/[,;]/).map((keyword) => keyword.trim()) ?? [] : value === void 0 ? [] : [value];
120
+ return [wanted].flat().map(String).some((candidate) => actual.includes(name === "status" ? candidate.toLowerCase() : candidate));
121
+ }
122
+ const collator = new Intl.Collator(void 0, { numeric: true, sensitivity: "base" });
123
+ function sortByText(entries, value, order) {
124
+ const keyed = entries.map((entry) => ({ entry, value: value(entry) }));
125
+ const sign = order === "asc" ? 1 : -1;
126
+ return keyed.sort((a, b) => a.value === void 0 ? b.value === void 0 ? 0 : 1 : b.value === void 0 ? -1 : sign * collator.compare(a.value, b.value)).map((item) => item.entry);
127
+ }
128
+ function sortEntries(bib, entries, list, schema) {
129
+ if (list.keys || list.sort === "source") return entries;
130
+ if (list.sort === "date" || list.sort === "year") {
131
+ const dated = entries.filter((entry) => entry.year !== null);
132
+ return [...bib.sort(dated, { by: list.sort, order: list.order }), ...entries.filter((entry) => entry.year === null)];
133
+ }
134
+ return sortByText(entries, (entry) => valueOf(entry, list.sort, schema), list.order);
135
+ }
136
+ function group(entries, list, schema) {
137
+ const by = list.groupBy;
138
+ if (!by) return entries.length ? [{ entries }] : [];
139
+ if (by === "status") {
140
+ return schema.statuses.map((status) => ({ label: status.label, entries: entries.filter((entry) => publicationStatus(entry, schema) === status.id) })).filter((selected) => selected.entries.length);
141
+ }
142
+ const values = [...new Set(entries.map((entry) => valueOf(entry, by, schema)).filter((value) => value !== void 0))];
143
+ values.sort(by === "year" ? (a, b) => Number(b) - Number(a) : collator.compare);
144
+ const groups = values.map((value) => ({ label: value, entries: entries.filter((entry) => valueOf(entry, by, schema) === value) }));
145
+ const other = entries.filter((entry) => valueOf(entry, by, schema) === void 0);
146
+ return other.length ? [...groups, { label: list.otherLabel, entries: other }] : groups;
147
+ }
148
+ function selectionSteps(bib, name, list, schema) {
149
+ const fields = [...Object.keys(list.where), list.sort, list.groupBy].filter((candidate) => !!candidate && !DERIVED.has(candidate) && !["date", "source"].includes(candidate));
150
+ for (const candidate of new Set(fields)) {
151
+ if (bib.entries.length && !bib.entries.some((entry) => plain(entry.raw[candidate]) !== void 0)) {
152
+ fail(LISTS_FILE, `${name}: no publication has a field "${candidate}"`);
153
+ }
154
+ }
155
+ const steps = [];
156
+ if (list.keys) {
157
+ const byKey = new Map(bib.entries.map((entry) => [entry.key, entry]));
158
+ const missing = list.keys.filter((key) => !byKey.has(key));
159
+ if (missing.length) fail(LISTS_FILE, `${name}.keys: no publication ${missing.join(", ")} in content/publications.bib`);
160
+ steps.push({ rule: `keys: ${list.keys.length}`, apply: () => list.keys.map((key) => byKey.get(key)) });
161
+ }
162
+ for (const [field2, wanted] of Object.entries(list.where)) {
163
+ steps.push({ rule: `where ${field2}: ${[wanted].flat().join(", ")}`, apply: (entries) => entries.filter((entry) => matches(entry, field2, wanted, schema)) });
164
+ }
165
+ if (list.since !== void 0) steps.push({ rule: `since: ${list.since}`, apply: (entries) => entries.filter((entry) => entry.year !== null && entry.year >= list.since) });
166
+ if (list.until !== void 0) steps.push({ rule: `until: ${list.until}`, apply: (entries) => entries.filter((entry) => entry.year !== null && entry.year <= list.until) });
167
+ steps.push({ rule: "sort", apply: (entries) => sortEntries(bib, entries, list, schema) });
168
+ if (list.limit !== void 0) steps.push({ rule: `limit: ${list.limit}`, apply: (entries) => entries.slice(0, list.limit) });
169
+ return steps;
170
+ }
171
+ function selectPublications(bib, name, list, schema) {
172
+ let entries = bib.entries;
173
+ for (const step of selectionSteps(bib, name, list, schema)) entries = step.apply(entries);
174
+ return group(entries, list, schema);
175
+ }
176
+ function explainListSelection(bib, name, list, schema) {
177
+ let entries = bib.entries;
178
+ const steps = [];
179
+ for (const step of selectionSteps(bib, name, list, schema)) {
180
+ const next = step.apply(entries);
181
+ steps.push({ rule: step.rule, removed: entries.length - next.length });
182
+ entries = next;
183
+ }
184
+ return { total: bib.entries.length, steps: steps.filter((step) => step.removed > 0), keys: group(entries, list, schema).flatMap((selected) => selected.entries.map((entry) => entry.key)) };
185
+ }
186
+ function describeList(list) {
187
+ const parts = [];
188
+ if (list.keys) parts.push(`${list.keys.length} chosen by key`);
189
+ for (const [name, wanted] of Object.entries(list.where)) parts.push(`${name} ${[wanted].flat().join(" or ")}`);
190
+ if (list.since !== void 0) parts.push(`since ${list.since}`);
191
+ if (list.until !== void 0) parts.push(`until ${list.until}`);
192
+ if (!list.keys) {
193
+ if (list.sort === "source") parts.push("in .bib order");
194
+ else if (list.sort === "date" || list.sort === "year") parts.push(list.order === "desc" ? "newest first" : "oldest first");
195
+ else parts.push(`by ${list.sort}${list.order === "desc" ? ", descending" : ""}`);
196
+ }
197
+ if (list.limit !== void 0) parts.push(`at most ${list.limit}`);
198
+ if (list.groupBy) parts.push(`grouped by ${list.groupBy}`);
199
+ return parts.join(" \xB7 ");
200
+ }
201
+
202
+ export { LISTS_FILE, LISTS_HEADER, describeList, explainListSelection, loadPublicationLists, publicationListNames, publicationStatus, resolveList, selectPublications };
@@ -1,6 +1,6 @@
1
- import { s as section, t as text, a as richText, u as url, l as linked, d as dateRange, b as absoluteUrl, r as requiredLink } from '../shared/vitakit.BerUaxtF.mjs';
1
+ import { s as section, t as text, a as richText, u as url, l as linked, d as dateRange, b as absoluteUrl, r as requiredLink } from '../shared/vitakit.DvABWuUc.mjs';
2
2
  import { z } from 'zod';
3
- import '../shared/vitakit.DbSeENVS.mjs';
3
+ import '../shared/vitakit.DCK90zq4.mjs';
4
4
  import 'node:url';
5
5
  import '../shared/vitakit.D10HyDhF.mjs';
6
6
 
@@ -2,14 +2,14 @@ import { readdir, readFile, stat } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { parse } from 'yaml';
4
4
  import { schemas } from './schemas.mjs';
5
- import { f as fail, a as formatZodIssues, i as importProjectModule } from '../shared/vitakit.DbSeENVS.mjs';
6
- import { c as renderRichFields } from '../shared/vitakit.BerUaxtF.mjs';
5
+ import { f as fail, a as formatZodIssues, i as importProjectModule } from '../shared/vitakit.DCK90zq4.mjs';
6
+ import { c as renderRichFields } from '../shared/vitakit.DvABWuUc.mjs';
7
7
  import { r as resolveSitePaths } from '../shared/vitakit.D10HyDhF.mjs';
8
8
  import { a as atMost } from '../shared/vitakit.TSMTC5Jl.mjs';
9
9
  import 'zod';
10
10
  import 'node:url';
11
11
 
12
- const RESERVED_SECTION_NAMES = /* @__PURE__ */ new Set(["site", "profile"]);
12
+ const RESERVED_SECTION_NAMES = /* @__PURE__ */ new Set(["site", "profile", "publication-lists"]);
13
13
  const SECTION_NAME = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
14
14
  function sectionNameProblem(id) {
15
15
  if (RESERVED_SECTION_NAMES.has(id)) return `"${id}" is reserved and cannot be a section`;
@@ -1,37 +1,39 @@
1
1
  import { join, sep, isAbsolute, relative } from 'node:path';
2
2
  import { parseExpression } from '@babel/parser';
3
- import { l as loadContent } from '../shared/vitakit.By5VWQ1n.mjs';
3
+ import { l as loadContent } from '../shared/vitakit.DteA9vLy.mjs';
4
4
  import { inventory } from './inventory.mjs';
5
- import { m as messageOf, w as whereOf } from '../shared/vitakit.DbSeENVS.mjs';
6
- import { assertTemplatesAreUsed, renderPublications, renderSection } from './emit.mjs';
5
+ import { m as messageOf, w as whereOf } from '../shared/vitakit.DCK90zq4.mjs';
6
+ import { assertTemplatesAreUsed, renderSection, renderPublications } from './emit.mjs';
7
7
  import 'node:fs/promises';
8
8
  import 'gray-matter';
9
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';
10
15
  import 'unified';
16
+ import 'rehype-parse';
17
+ import 'rehype-sanitize';
18
+ import 'rehype-stringify';
11
19
  import 'remark-parse';
12
20
  import 'remark-gfm';
13
21
  import 'remark-math';
14
22
  import 'remark-rehype';
15
23
  import 'rehype-katex';
16
24
  import 'rehype-raw';
17
- import 'rehype-stringify';
18
25
  import 'unist-util-visit';
19
- import '../shared/vitakit.D10HyDhF.mjs';
20
- import 'node:fs';
21
- import '../shared/vitakit.DQ6PmGoL.mjs';
22
- import 'node:url';
23
- import '@behackl/citation-js-extras';
24
- import 'rehype-parse';
25
- import 'rehype-sanitize';
26
26
  import 'zod';
27
+ import './publication-lists.mjs';
28
+ import '../shared/vitakit.D10HyDhF.mjs';
27
29
  import './sections.mjs';
28
30
  import './schemas.mjs';
29
- import '../shared/vitakit.BerUaxtF.mjs';
31
+ import '../shared/vitakit.DvABWuUc.mjs';
30
32
  import '../shared/vitakit.TSMTC5Jl.mjs';
31
33
  import './fields.mjs';
32
34
  import './documents.mjs';
35
+ import '../shared/vitakit.DvvUYUTS.mjs';
33
36
  import '../shared/vitakit.D-DdJaDZ.mjs';
34
- import '../shared/vitakit.Do5HC_MD.mjs';
35
37
  import 'eta';
36
38
  import '../shared/vitakit.DcQOdM4I.mjs';
37
39
  import '../shared/vitakit.CfB3hMHl.mjs';
@@ -96,9 +98,7 @@ ${problem.message}`;
96
98
  for (const document of inv.documents) {
97
99
  for (const placement of document.placements) {
98
100
  try {
99
- if (placement.section === "publications") {
100
- await renderPublications(content.publications, document.options, templateDir, content.profile.url);
101
- } else if (valid.has(placement.section)) {
101
+ if (valid.has(placement.section)) {
102
102
  await renderSection(placement.section, content.sections[placement.section] ?? [], document.options, templateDir);
103
103
  }
104
104
  } catch (error) {
@@ -109,6 +109,15 @@ ${problem.message}`;
109
109
  });
110
110
  }
111
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
+ }
112
121
  }
113
122
  }
114
123
  const { realpath } = await import('node:fs/promises');
@@ -1,38 +1,20 @@
1
1
  import { parse } from '@vue/compiler-sfc';
2
-
3
- function lineStart(source, offset) {
4
- return source.lastIndexOf("\n", offset - 1) + 1;
5
- }
6
- function lineEnd(source, offset) {
7
- const end = source.indexOf("\n", offset);
8
- return end === -1 ? source.length : end;
9
- }
10
- function indentationAt(source, offset) {
11
- return /^[ \t]*/.exec(source.slice(lineStart(source, offset)))[0];
12
- }
13
- function insertLinesAfter(source, offset, lines) {
14
- const end = lineEnd(source, offset);
15
- return `${source.slice(0, end)}
16
- ${lines.join("\n")}${source.slice(end)}`;
17
- }
18
- function insertLinesBefore(source, offset, lines) {
19
- const start = lineStart(source, offset);
20
- return `${source.slice(0, start)}${lines.join("\n")}
21
- ${source.slice(start)}`;
22
- }
2
+ import { l as lineStart, a as lineEnd, i as indentationAt, b as insertLinesAfter, c as insertLinesBefore } from '../shared/vitakit.UfoXdL7U.mjs';
23
3
 
24
4
  const ELEMENT = 1;
25
5
  const ATTRIBUTE = 6;
26
6
  const pascal = (name) => name.split("-").map((part) => part[0].toUpperCase() + part.slice(1)).join("");
27
- function literalId(node) {
7
+ function literalAttribute(node, name) {
28
8
  for (const prop of node.props ?? []) {
29
- if (prop.type === ATTRIBUTE && prop.name === "id") return prop.value?.content;
30
- if (prop.type !== ATTRIBUTE && prop.name === "bind" && prop.arg?.content === "id") {
9
+ if (prop.type === ATTRIBUTE && prop.name === name) return prop.value?.content;
10
+ if (prop.type !== ATTRIBUTE && prop.name === "bind" && prop.arg?.content === name) {
31
11
  return /^\s*(['"`])([^'"`$]*)\1\s*$/.exec(prop.exp?.content ?? "")?.[2];
32
12
  }
33
13
  }
34
14
  return void 0;
35
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));
36
18
  function sectionElements(source, components = []) {
37
19
  const known = /* @__PURE__ */ new Set(["VitaSection", ...components]);
38
20
  let ast;
@@ -44,21 +26,35 @@ function sectionElements(source, components = []) {
44
26
  return { ok: false, reason: `cannot be parsed: ${error instanceof Error ? error.message : String(error)}` };
45
27
  }
46
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;
47
31
  const elements = [];
48
- const visit = (node) => {
32
+ const lists = [];
33
+ const visit = (node, anchor) => {
49
34
  if (node.type === ELEMENT && node.tag) {
50
35
  const tag = node.tag.includes("-") ? pascal(node.tag) : node.tag;
51
- const id = known.has(tag) ? literalId(node) : void 0;
52
- if (id) {
53
- const hasContent = (node.children ?? []).some((child) => child.type !== 3 && !(child.type === 2 && !child.content.trim()));
54
- elements.push({ id, tag: node.tag, start: node.loc.start.offset, end: node.loc.end.offset, line: node.loc.start.line, hasContent });
55
- }
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);
56
54
  }
57
- for (const child of node.children ?? []) visit(child);
58
55
  };
59
56
  visit(ast);
60
- const roots = (ast.children ?? []).filter((node) => node.type === ELEMENT);
61
- return { ok: true, value: { elements, ...roots.length === 1 ? { root: roots[0] } : {} } };
57
+ return { ok: true, value: { elements, lists, ...root ? { root } : {} } };
62
58
  }
63
59
  function attribute(value) {
64
60
  return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;");
@@ -66,16 +62,20 @@ function attribute(value) {
66
62
  function sectionElementSource(placement) {
67
63
  return `<${placement.component ?? "VitaSection"} id="${placement.id}"${placement.title ? ` title="${attribute(placement.title)}"` : ""} />`;
68
64
  }
69
- function addSectionToPage(source, placement, components = []) {
70
- const found = sectionElements(source, components);
71
- if (!found.ok) return found;
72
- const { elements, root } = found.value;
73
- if (elements.some((element2) => element2.id === placement.id)) return { ok: false, reason: `it already shows ${placement.id}` };
74
- const element = sectionElementSource(placement);
75
- const anchor = (placement.after && elements.find((existing) => existing.id === placement.after)) ?? elements[elements.length - 1];
76
- if (anchor) {
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
77
  const indent2 = indentationAt(source, anchor.start);
78
- return { ok: true, value: { source: insertLinesAfter(source, anchor.end - 1, [`${indent2}${element}`]), afterSection: anchor.id } };
78
+ return { ok: true, value: { source: insertLinesAfter(source, anchor.end - 1, [`${indent2}${markup}`]), afterSection: found.id } };
79
79
  }
80
80
  if (!root || !root.tag) return { ok: false, reason: "its template has no single root element to add to" };
81
81
  const close = source.lastIndexOf(`</${root.tag}`, root.loc.end.offset);
@@ -83,7 +83,22 @@ function addSectionToPage(source, placement, components = []) {
83
83
  return { ok: false, reason: `its <${root.tag}> does not end on a line of its own` };
84
84
  }
85
85
  const indent = `${indentationAt(source, close)} `;
86
- return { ok: true, value: { source: insertLinesBefore(source, close, ["", `${indent}${element}`]) } };
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);
87
102
  }
88
103
  function useComponentForSection(source, id, component, components = []) {
89
104
  const found = sectionElements(source, components);
@@ -104,13 +119,35 @@ function useComponentForSection(source, id, component, components = []) {
104
119
  }
105
120
  return { ok: true, value: { source: result, changed } };
106
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
+ }
107
152
 
108
- const vue = {
109
- __proto__: null,
110
- addSectionToPage: addSectionToPage,
111
- sectionElementSource: sectionElementSource,
112
- sectionElements: sectionElements,
113
- useComponentForSection: useComponentForSection
114
- };
115
-
116
- export { indentationAt as a, insertLinesBefore as b, lineEnd as c, addSectionToPage as d, insertLinesAfter as i, lineStart as l, sectionElementSource as s, vue as v };
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.mjs CHANGED
@@ -1,4 +1,4 @@
1
- export { a as aligned, c as commands, d as detectPackageManager, m as main } from './shared/vitakit.CoWX2Z4R.mjs';
1
+ export { a as aligned, c as commands, d as detectPackageManager, m as main } from './shared/vitakit.zwr1ssO5.mjs';
2
2
  import 'node:fs';
3
3
  import 'node:path';
4
4
  import './shared/vitakit.DQ6PmGoL.mjs';
package/dist/index.d.mts CHANGED
@@ -1,9 +1,9 @@
1
1
  import * as zod from 'zod';
2
2
  import { z } from 'zod';
3
3
  export { z } from 'zod';
4
- import { D as DateRange, V as Visibility, a as VitakitContent } from './shared/vitakit.BfpxDtz1.mjs';
4
+ import { D as DateRange, V as Visibility, a as VitakitContent } from './shared/vitakit.BfR_mUHm.mjs';
5
5
  import { RichTextMarker } from './shared/index.mjs';
6
- export { C as ContentIssue, b as DatePoint, E as Entry, c as EntryBase, L as Link, d as Linked, P as Page, e as Profile, f as Publication, g as PublicationBadge, h as PublicationGroup, i as RichText } from './shared/vitakit.BfpxDtz1.mjs';
6
+ export { C as ContentIssue, b as DatePoint, E as Entry, c as EntryBase, L as Link, d as Linked, P as Page, e as Profile, f as Publication, g as PublicationBadge, h as PublicationGroup, i as PublicationList, j as RichText } from './shared/vitakit.BfR_mUHm.mjs';
7
7
  import { BadgeConfig } from '@behackl/citation-js-extras';
8
8
  export { BadgeConfig, badgePresets } from '@behackl/citation-js-extras';
9
9
  import * as zod_v4_core from 'zod/v4/core';
@@ -696,18 +696,18 @@ interface LoadContentOptions {
696
696
  }
697
697
  declare function loadContent(rootDir: string, options?: LoadContentOptions): Promise<VitakitContent>;
698
698
 
699
- interface PublicationGroupConfig {
699
+ interface PublicationStatusConfig {
700
700
  /** A value of the `status` field; lowercase, e.g. `preprint` or `talk-slides`. */
701
701
  id: string;
702
- /** Heading on the website and in the PDF. */
702
+ /** Heading of its group, on the website and in the PDF. */
703
703
  label: string;
704
704
  }
705
705
  /** What `schemas/publications.ts` may set; everything is optional. */
706
706
  interface PublicationsInput {
707
- /** The values of `status`, in the order the groups appear. */
708
- groups?: PublicationGroupConfig[];
709
- /** The group of an entry without `status`. Defaults to the first group. */
710
- defaultGroup?: string;
707
+ /** The values of `status`, in the order their groups appear. */
708
+ statuses?: PublicationStatusConfig[];
709
+ /** The status of an entry without one. Defaults to the first status. */
710
+ defaultStatus?: string;
711
711
  /** BibTeX fields that become links after a reference, on the website. */
712
712
  badges?: BadgeConfig[];
713
713
  /** Further BibTeX fields to hand to the website, as `entry.fields`. */
@@ -725,4 +725,4 @@ interface PublicationsInput {
725
725
  declare function publications(input: PublicationsInput): PublicationsInput;
726
726
 
727
727
  export { DateRange, Visibility, VitakitContent, dateRange, _default$4 as education, _default$3 as employment, entry, extendSection, linked, loadContent, publications, requiredLink, richText, schemas, section, _default as supervision, _default$1 as talks, _default$2 as teaching, text, url, visibility };
728
- export type { BuiltinSection, LoadContentOptions, PublicationGroupConfig, PublicationsInput };
728
+ export type { BuiltinSection, LoadContentOptions, PublicationStatusConfig, PublicationsInput };
package/dist/index.mjs CHANGED
@@ -1,27 +1,29 @@
1
- export { d as dateRange, e as entry, l as linked, r as requiredLink, a as richText, s as section, t as text, u as url, v as visibility } from './shared/vitakit.BerUaxtF.mjs';
1
+ export { d as dateRange, e as entry, l as linked, r as requiredLink, a as richText, s as section, t as text, u as url, v as visibility } from './shared/vitakit.DvABWuUc.mjs';
2
2
  export { education, employment, extendSection, schemas, supervision, talks, teaching } from './chunks/schemas.mjs';
3
- export { l as loadContent, p as publications } from './shared/vitakit.By5VWQ1n.mjs';
3
+ export { l as loadContent } from './shared/vitakit.DteA9vLy.mjs';
4
+ export { p as publications } from './chunks/bibliography.mjs';
4
5
  export { badgePresets } from '@behackl/citation-js-extras';
5
6
  export { z } from 'zod';
6
- import './shared/vitakit.DbSeENVS.mjs';
7
+ import './shared/vitakit.DCK90zq4.mjs';
7
8
  import 'node:url';
8
9
  import './shared/vitakit.D10HyDhF.mjs';
9
10
  import 'node:fs/promises';
10
11
  import 'node:path';
11
12
  import 'gray-matter';
12
13
  import 'yaml';
14
+ import './chunks/publication-lists.mjs';
15
+ import './chunks/sections.mjs';
16
+ import './shared/vitakit.TSMTC5Jl.mjs';
17
+ import 'node:fs';
18
+ import './shared/vitakit.DQ6PmGoL.mjs';
13
19
  import 'unified';
20
+ import 'rehype-parse';
21
+ import 'rehype-sanitize';
22
+ import 'rehype-stringify';
14
23
  import 'remark-parse';
15
24
  import 'remark-gfm';
16
25
  import 'remark-math';
17
26
  import 'remark-rehype';
18
27
  import 'rehype-katex';
19
28
  import 'rehype-raw';
20
- import 'rehype-stringify';
21
29
  import 'unist-util-visit';
22
- import 'node:fs';
23
- import './shared/vitakit.DQ6PmGoL.mjs';
24
- import 'rehype-parse';
25
- import 'rehype-sanitize';
26
- import './chunks/sections.mjs';
27
- import './shared/vitakit.TSMTC5Jl.mjs';