@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
@@ -2,7 +2,8 @@ import { readdir, readFile } from 'node:fs/promises';
2
2
  import { basename, join } from 'node:path';
3
3
  import { parse } from 'yaml';
4
4
  import { z } from 'zod';
5
- import { f as fail, m as messageOf, a as formatZodIssues } from '../shared/vitakit.DbSeENVS.mjs';
5
+ import { f as fail, m as messageOf, a as formatZodIssues } from '../shared/vitakit.DCK90zq4.mjs';
6
+ import { c as findPdfPublicationPlacements, f as findPdfPlacements } from '../shared/vitakit.DvvUYUTS.mjs';
6
7
  import { a as atMost } from '../shared/vitakit.TSMTC5Jl.mjs';
7
8
  import { f as fieldValue, a as filterWhere } from '../shared/vitakit.D-DdJaDZ.mjs';
8
9
  import 'node:url';
@@ -37,10 +38,7 @@ const documentSchema = z.strictObject({
37
38
  * build (see `vitakit verify`), and the website renders CV data directly.
38
39
  */
39
40
  visibility: z.enum(["public", "cv", "private"]).default("cv"),
40
- sections: z.record(z.string(), sectionOptions).default({}),
41
- publications: z.strictObject({
42
- groups: z.array(z.string()).optional()
43
- }).default({})
41
+ sections: z.record(z.string(), sectionOptions).default({})
44
42
  });
45
43
  const DEFAULTS = documentSchema.parse({});
46
44
  async function findDocuments(cvDir) {
@@ -62,6 +60,10 @@ async function findDocuments(cvDir) {
62
60
  problems.push({ where, message: messageOf(error) });
63
61
  continue;
64
62
  }
63
+ if (typeof data === "object" && data !== null && Object.hasOwn(data, "publications")) {
64
+ problems.push({ where, message: "publications: which publications a PDF shows is now a list in content/publication-lists.yml, placed with \\vitapublications[Heading]{list}; `vitakit migrate` converts it" });
65
+ continue;
66
+ }
65
67
  const parsed = documentSchema.safeParse(data ?? {});
66
68
  if (!parsed.success) {
67
69
  problems.push({ where, message: `invalid
@@ -70,7 +72,14 @@ ${formatZodIssues(parsed.error)}` });
70
72
  }
71
73
  options = parsed.data;
72
74
  }
73
- documents.push({ name, texFile: join(cvDir, file), options });
75
+ const tex = `cv-tex/${file}`;
76
+ documents.push({
77
+ name,
78
+ texFile: join(cvDir, file),
79
+ options,
80
+ placements: findPdfPlacements(name, tex, source),
81
+ publications: findPdfPublicationPlacements(name, tex, source)
82
+ });
74
83
  }
75
84
  return { documents, problems };
76
85
  }
@@ -1,8 +1,8 @@
1
1
  import { z } from 'zod';
2
- import { s as section, t as text, d as dateRange, l as linked, u as url, a as richText } from '../shared/vitakit.BerUaxtF.mjs';
3
- import { p as propertyKey } from '../shared/vitakit.BZZIsWlY.mjs';
2
+ import { s as section, t as text, d as dateRange, l as linked, u as url, a as richText } from '../shared/vitakit.DvABWuUc.mjs';
3
+ import { p as propertyKey } from '../shared/vitakit.CCTDg7pz.mjs';
4
4
  import { i as isSitePath } from '../shared/vitakit.D10HyDhF.mjs';
5
- import '../shared/vitakit.DbSeENVS.mjs';
5
+ import '../shared/vitakit.DCK90zq4.mjs';
6
6
  import 'node:url';
7
7
  import '@babel/parser';
8
8
 
@@ -2,7 +2,7 @@ import { existsSync } from 'node:fs';
2
2
  import { readdir, readFile } from 'node:fs/promises';
3
3
  import { basename, join } from 'node:path';
4
4
  import { r as resolveAsset } from '../shared/vitakit.DQ6PmGoL.mjs';
5
- import { C as CommandError } from '../shared/vitakit.CoWX2Z4R.mjs';
5
+ import { C as CommandError } from '../shared/vitakit.zwr1ssO5.mjs';
6
6
  import 'node:url';
7
7
  import 'node:process';
8
8
  import 'node:util';
@@ -15,7 +15,7 @@ const SCOPES = {
15
15
  VitaLinks: "the profile links",
16
16
  VitaMarkdown: "every Markdown page body",
17
17
  VitaIssues: "the development error banner",
18
- VitaPublications: "the publication list and its groups",
18
+ VitaPublications: "how a publication list is laid out: its headings and groups",
19
19
  VitaPublication: "how each reference looks on the website"
20
20
  };
21
21
  async function ejectableAssets() {
@@ -5,12 +5,13 @@ import { Eta } from 'eta';
5
5
  import { r as resolveAsset } from '../shared/vitakit.DQ6PmGoL.mjs';
6
6
  import { b as escapeUrl, a as escapeTex, e as escapeRichTex } from '../shared/vitakit.DcQOdM4I.mjs';
7
7
  import { f as formatDateRange, e as entrySecondaryParts, b as entryTitleField } from '../shared/vitakit.CfB3hMHl.mjs';
8
- import { f as fail } from '../shared/vitakit.DbSeENVS.mjs';
8
+ import { f as fail } from '../shared/vitakit.DCK90zq4.mjs';
9
9
  import { i as isSitePath } from '../shared/vitakit.D10HyDhF.mjs';
10
10
  import { selectEntries } from './documents.mjs';
11
11
  import 'node:url';
12
12
  import 'yaml';
13
13
  import 'zod';
14
+ import '../shared/vitakit.DvvUYUTS.mjs';
14
15
  import '../shared/vitakit.TSMTC5Jl.mjs';
15
16
  import '../shared/vitakit.D-DdJaDZ.mjs';
16
17
 
@@ -114,13 +115,24 @@ function genericHelpers(h) {
114
115
  }
115
116
  };
116
117
  }
117
- function bibgroup(group, where) {
118
- const unsafe = /[\s%#\\{},~]/;
119
- const bad = [group.id, ...group.entries.map((entry) => entry.key)].find((value) => !value || unsafe.test(value));
118
+ function texSafe(values, where) {
119
+ const bad = values.find((value) => !value || /[\s%#\\{},~]/.test(value));
120
120
  if (bad !== void 0) {
121
121
  fail(where, `"${bad}" cannot be used as a citation key or group id in LaTeX; use letters, digits and - _ : . / +`);
122
122
  }
123
- return fragment(`\\vitabibgroup{${group.id}}{${group.entries.map((entry) => entry.key).join(",")}}`);
123
+ }
124
+ function bibHelpers(list, where) {
125
+ return {
126
+ cite() {
127
+ const keys = list.groups.flatMap((group) => group.entries.map((entry) => entry.key));
128
+ texSafe(keys, where);
129
+ return fragment(`\\nocite{${keys.join(",")}}`);
130
+ },
131
+ group(group) {
132
+ texSafe([group.id, ...group.entries.map((entry) => entry.key)], where);
133
+ return fragment(`\\vitabibgroup{${group.id}}{${group.entries.map((entry) => entry.key).join(",")}}`);
134
+ }
135
+ };
124
136
  }
125
137
  async function resolveTemplate(id, userDir, { exclude } = {}) {
126
138
  const candidates = [
@@ -159,17 +171,16 @@ async function renderSection(id, entries, options, templateDir) {
159
171
  throw new Error(`template ${id}.eta: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
160
172
  }
161
173
  }
162
- async function renderPublications(publications, options, templateDir, siteUrl) {
163
- const groups = options.publications.groups ? options.publications.groups.map((id) => publications.find((group) => group.id === id)).filter((group) => group !== void 0) : publications;
174
+ async function renderPublications(list, templateDir, siteUrl) {
164
175
  if (!siteUrl) {
165
- const local = groups.flatMap((group) => group.entries).find((entry) => entry.titleUrl && isSitePath(entry.titleUrl));
176
+ const local = list.groups.flatMap((group) => group.entries).find((entry) => entry.titleUrl && isSitePath(entry.titleUrl));
166
177
  if (local) fail("content/publications.bib", `${local.key}: url ${local.titleUrl} ${SITE_PATH_HELP}`);
167
178
  }
168
179
  const { path: templatePath, source } = await resolveTemplate("publications", templateDir);
169
180
  return tidy(eta.renderString(source, {
170
- groups,
181
+ groups: list.groups,
171
182
  ...createTemplateHelpers(templatePath, "content/publications.bib"),
172
- bib: { group: (group) => bibgroup(group, templatePath) }
183
+ bib: bibHelpers(list, templatePath)
173
184
  }));
174
185
  }
175
186
  async function emitDocument(emit) {
@@ -177,22 +188,27 @@ async function emitDocument(emit) {
177
188
  await assertTemplatesAreUsed(emit.templateDir, Object.keys(emit.sections));
178
189
  const written = [];
179
190
  for (const [id, entries] of Object.entries(emit.sections)) {
180
- const body2 = await renderSection(id, entries, emit.options, emit.templateDir);
181
- const file2 = join(emit.outDir, `${id}.tex`);
182
- await writeFile(file2, `% Generated by vitakit. Do not edit; edit content/${id}.yml instead.
183
- ${body2.trim()}
191
+ const body = await renderSection(id, entries, emit.options, emit.templateDir);
192
+ const file = join(emit.outDir, `${id}.tex`);
193
+ await writeFile(file, `% Generated by vitakit. Do not edit; edit content/${id}.yml instead.
194
+ ${body.trim()}
184
195
  `);
185
- written.push(file2);
196
+ written.push(file);
197
+ }
198
+ const current = /* @__PURE__ */ new Set(["profile.tex", ...written.map((file) => basename(file))]);
199
+ for (const file of await readdir(emit.outDir)) {
200
+ if (file.endsWith(".tex") && !current.has(file)) await rm(join(emit.outDir, file));
186
201
  }
187
- const body = await renderPublications(emit.publications, emit.options, emit.templateDir, emit.siteUrl);
188
- const file = join(emit.outDir, "publications.tex");
189
- await writeFile(file, `% Generated by vitakit. Do not edit; edit content/publications.bib instead.
202
+ const listDir = join(emit.outDir, "publications");
203
+ await rm(listDir, { recursive: true, force: true });
204
+ await mkdir(listDir);
205
+ for (const [name, list] of Object.entries(emit.publications)) {
206
+ const body = await renderPublications(list, emit.templateDir, emit.siteUrl);
207
+ const file = join(listDir, `${name}.tex`);
208
+ await writeFile(file, `% Generated by vitakit. Do not edit; edit content/publications.bib or content/publication-lists.yml instead.
190
209
  ${body.trim()}
191
210
  `);
192
- written.push(file);
193
- const current = /* @__PURE__ */ new Set(["profile.tex", ...written.map((file2) => basename(file2))]);
194
- for (const file2 of await readdir(emit.outDir)) {
195
- if (file2.endsWith(".tex") && !current.has(file2)) await rm(join(emit.outDir, file2));
211
+ written.push(file);
196
212
  }
197
213
  return written;
198
214
  }
@@ -1,6 +1,6 @@
1
1
  import { z } from 'zod';
2
- import { v as visibility, t as text, a as richText, d as dateRange, u as url, b as absoluteUrl, l as linked, r as requiredLink } from '../shared/vitakit.BerUaxtF.mjs';
3
- import '../shared/vitakit.DbSeENVS.mjs';
2
+ import { v as visibility, t as text, a as richText, d as dateRange, u as url, b as absoluteUrl, l as linked, r as requiredLink } from '../shared/vitakit.DvABWuUc.mjs';
3
+ import '../shared/vitakit.DCK90zq4.mjs';
4
4
  import 'node:url';
5
5
  import '../shared/vitakit.D10HyDhF.mjs';
6
6
 
@@ -3,30 +3,32 @@ import { existsSync } from 'node:fs';
3
3
  import { join, relative, sep } from 'node:path';
4
4
  import matter from 'gray-matter';
5
5
  import { parse } from 'yaml';
6
- import { l as loadContent } from '../shared/vitakit.By5VWQ1n.mjs';
6
+ import { l as loadContent } from '../shared/vitakit.DteA9vLy.mjs';
7
7
  import { describeSection } from './fields.mjs';
8
8
  import { findSectionFiles, resolveSectionSchema } from './sections.mjs';
9
9
  import { schemas } from './schemas.mjs';
10
10
  import { findDocuments, documentSectionProblems } from './documents.mjs';
11
- import { p as pageRoute, d as documentDirective, f as findPdfPlacements, a as findWebPlacements } from '../shared/vitakit.Do5HC_MD.mjs';
12
- import { m as messageOf, w as whereOf } from '../shared/vitakit.DbSeENVS.mjs';
11
+ import { a as pageRoute, d as documentDirective, p as publicationPlacementProblems, b as findWebPlacements } from '../shared/vitakit.DvvUYUTS.mjs';
12
+ import { m as messageOf, w as whereOf } from '../shared/vitakit.DCK90zq4.mjs';
13
+ import './bibliography.mjs';
14
+ import '../shared/vitakit.DQ6PmGoL.mjs';
15
+ import 'node:url';
16
+ import '@behackl/citation-js-extras';
13
17
  import 'unified';
18
+ import 'rehype-parse';
19
+ import 'rehype-sanitize';
20
+ import 'rehype-stringify';
14
21
  import 'remark-parse';
15
22
  import 'remark-gfm';
16
23
  import 'remark-math';
17
24
  import 'remark-rehype';
18
25
  import 'rehype-katex';
19
26
  import 'rehype-raw';
20
- import 'rehype-stringify';
21
27
  import 'unist-util-visit';
22
- import '../shared/vitakit.D10HyDhF.mjs';
23
- import '../shared/vitakit.DQ6PmGoL.mjs';
24
- import 'node:url';
25
- import '@behackl/citation-js-extras';
26
- import 'rehype-parse';
27
- import 'rehype-sanitize';
28
28
  import 'zod';
29
- import '../shared/vitakit.BerUaxtF.mjs';
29
+ import './publication-lists.mjs';
30
+ import '../shared/vitakit.D10HyDhF.mjs';
31
+ import '../shared/vitakit.DvABWuUc.mjs';
30
32
  import '../shared/vitakit.TSMTC5Jl.mjs';
31
33
  import '../shared/vitakit.D-DdJaDZ.mjs';
32
34
 
@@ -128,17 +130,18 @@ async function inventory(rootDir, options = {}) {
128
130
  }
129
131
  page(pageRoute(below)).vue = posix(relative(rootDir, path));
130
132
  }
131
- const bib = join(rootDir, "content", "publications.bib");
132
- const publications = existsSync(bib) ? {
133
+ const bib = await readFile(join(rootDir, "content", "publications.bib"), "utf8").catch(() => void 0);
134
+ const publications = bib === void 0 ? void 0 : {
133
135
  file: "content/publications.bib",
134
- entries: content.publications.reduce((sum, group) => sum + group.entries.length, 0),
135
- groups: content.publications.length
136
- } : void 0;
136
+ // Counted, not parsed: the lists were parsed above, and may be broken.
137
+ entries: bib.match(/^\s*@(?!comment\b|string\b|preamble\b)\w+\s*[{(]/gim)?.length ?? 0,
138
+ counts: Object.fromEntries(Object.entries(content.publications).map(([name, list]) => [name, list.count]))
139
+ };
137
140
  const cvDir = join(rootDir, "cv-tex");
138
141
  const { documents: found, problems: documentProblems } = await findDocuments(cvDir);
139
142
  problems.push(...documentProblems);
140
- const known = /* @__PURE__ */ new Set([...sections.map((section) => section.id), "publications", "profile"]);
141
- const groupIds = new Set(content.publications.map((group) => group.id));
143
+ const known = /* @__PURE__ */ new Set([...sections.map((section) => section.id), "profile"]);
144
+ const lists = Object.keys(content.publications);
142
145
  const documents = [];
143
146
  for (const document of found) {
144
147
  const tex = `cv-tex/${document.name}.tex`;
@@ -151,17 +154,13 @@ async function inventory(rootDir, options = {}) {
151
154
  else if (directive.name !== document.name) {
152
155
  own.push({ where: `${tex}:${directive.line}`, message: `\\vitadocument{${directive.name}} does not match the file name; expected \\vitadocument{${document.name}}` });
153
156
  }
154
- const placements = findPdfPlacements(document.name, tex, source);
157
+ const { placements } = document;
155
158
  for (const placement of placements) {
156
- if (!known.has(placement.section)) {
159
+ if (!known.has(placement.section) && placement.section !== "publications") {
157
160
  own.push({ where: `${tex}:${placement.line}`, message: `no section called "${placement.section}"` });
158
161
  }
159
162
  }
160
- for (const group of document.options.publications.groups ?? []) {
161
- if (!groupIds.has(group) && publications) {
162
- own.push({ where: `cv-tex/${document.name}.yml`, message: `publications.groups: no publication group "${group}"` });
163
- }
164
- }
163
+ own.push(...publicationPlacementProblems([], [...placements, ...document.publications], lists));
165
164
  problems.push(...own);
166
165
  documents.push({
167
166
  name: document.name,
@@ -169,6 +168,7 @@ async function inventory(rootDir, options = {}) {
169
168
  ...existsSync(join(cvDir, `${document.name}.yml`)) ? { yml: `cv-tex/${document.name}.yml` } : {},
170
169
  options: document.options,
171
170
  placements,
171
+ publications: document.publications,
172
172
  ...pdf ? { pdf: { file: `cv-tex/${document.name}.pdf`, mtimeMs: pdf.mtimeMs } } : {},
173
173
  problems: [...documentProblems.filter((problem) => problem.where === `cv-tex/${document.name}.yml`), ...own]
174
174
  });
@@ -178,10 +178,12 @@ async function inventory(rootDir, options = {}) {
178
178
  ...await files(join(rootDir, "schemas"), () => true),
179
179
  ...await files(cvDir, (name) => /\.(tex|yml|yaml|eta|cls)$/.test(name)).then((list) => list.filter((path) => !relative(cvDir, path).startsWith("generated")))
180
180
  ]);
181
- const web = options.web === false ? { placements: [], dynamic: [], components: /* @__PURE__ */ new Map() } : await findWebPlacements(rootDir).catch((error) => {
181
+ const none = () => ({ placements: [], publications: [], dynamic: [], components: /* @__PURE__ */ new Map() });
182
+ const web = options.web === false ? none() : await findWebPlacements(rootDir).catch((error) => {
182
183
  problems.push({ where: whereOf(error, "app/"), message: messageOf(error) });
183
- return { placements: [], dynamic: [], components: /* @__PURE__ */ new Map() };
184
+ return none();
184
185
  });
186
+ problems.push(...publicationPlacementProblems(web.publications, [], lists));
185
187
  return {
186
188
  root: rootDir,
187
189
  sections,
@@ -0,0 +1,218 @@
1
+ import { readdir, stat } from 'node:fs/promises';
2
+ import { join, relative, sep } from 'node:path';
3
+ import { parseExpression } from '@babel/parser';
4
+ import { parse, parseDocument, isMap, isScalar } from 'yaml';
5
+ import { l as loadPublicationsSchema, d as defaultPublications } from './bibliography.mjs';
6
+ import { i as importProjectModule } from '../shared/vitakit.DCK90zq4.mjs';
7
+ import { addPublicationList, publicationListYaml } from './yaml.mjs';
8
+ import { r as renamePublicationsSettings } from '../shared/vitakit.CCTDg7pz.mjs';
9
+ import { publicationsElements } from './vue.mjs';
10
+ import { f as findPdfPlacements } from '../shared/vitakit.DvvUYUTS.mjs';
11
+ import { LISTS_FILE, LISTS_HEADER } from './publication-lists.mjs';
12
+ import 'node:fs';
13
+ import '../shared/vitakit.DQ6PmGoL.mjs';
14
+ import 'node:url';
15
+ import '@behackl/citation-js-extras';
16
+ import 'unified';
17
+ import 'rehype-parse';
18
+ import 'rehype-sanitize';
19
+ import 'rehype-stringify';
20
+ import 'remark-parse';
21
+ import 'remark-gfm';
22
+ import 'remark-math';
23
+ import 'remark-rehype';
24
+ import 'rehype-katex';
25
+ import 'rehype-raw';
26
+ import 'unist-util-visit';
27
+ import 'zod';
28
+ import '../shared/vitakit.UfoXdL7U.mjs';
29
+ import '@vue/compiler-sfc';
30
+
31
+ class ListPlan {
32
+ constructor(existing) {
33
+ this.existing = existing;
34
+ }
35
+ added = /* @__PURE__ */ new Map();
36
+ /** The name of a list with these settings: an existing one, or a new one. */
37
+ name(wanted, settings) {
38
+ const same = (other) => JSON.stringify(other ?? {}) === JSON.stringify(settings);
39
+ for (const [name2, other] of [...Object.entries(this.existing), ...this.added]) if (same(other)) return name2;
40
+ let name = wanted;
41
+ for (let index = 2; Object.hasOwn(this.existing, name) || this.added.has(name); index++) name = `${wanted}-${index}`;
42
+ this.added.set(name, settings);
43
+ return name;
44
+ }
45
+ }
46
+ function statusList(statuses, grouped = true) {
47
+ return {
48
+ wanted: `${statuses?.join("-") ?? "all"}${grouped ? "" : "-ungrouped"}`,
49
+ settings: {
50
+ ...statuses ? { where: { status: statuses.length === 1 ? statuses[0] : statuses } } : {},
51
+ ...grouped ? { groupBy: "status" } : {}
52
+ }
53
+ };
54
+ }
55
+ function literalStrings(expression) {
56
+ try {
57
+ const node = parseExpression(expression);
58
+ if (node.type !== "ArrayExpression") return void 0;
59
+ const values = node.elements.map((element) => element?.type === "StringLiteral" ? element.value : void 0);
60
+ return values.every((value) => value !== void 0) ? values : void 0;
61
+ } catch {
62
+ return void 0;
63
+ }
64
+ }
65
+ function legacyList(element, statuses) {
66
+ const groupsProp = element.props.find((prop) => prop.name === "groups");
67
+ const headingsProp = element.props.find((prop) => prop.name === "headings");
68
+ const groups = groupsProp?.expression !== void 0 ? literalStrings(groupsProp.expression) : void 0;
69
+ if (groupsProp && !groups) return { reason: "its groups are not a literal list such as ['preprint']" };
70
+ const headings = !headingsProp || headingsProp.expression === "true" || headingsProp.expression === void 0 && headingsProp.value === void 0;
71
+ if (headingsProp && !headings && headingsProp.expression !== "false") return { reason: "its headings are not literally true or false" };
72
+ const list = statusList(groups, headings);
73
+ const reordered = headings && groups && groups.join() !== statuses.filter((status) => groups.includes(status)).join();
74
+ const merged = !headings && (groups?.length ?? 2) > 1;
75
+ const note = reordered ? "its groups now appear in the order of the statuses in schemas/publications.ts" : merged ? "it is now one list, newest first, rather than one status after the other" : void 0;
76
+ return { ...list, ...note ? { note } : {} };
77
+ }
78
+ async function statusOrder(root) {
79
+ const ids = (statuses) => statuses.map((status) => status.id);
80
+ try {
81
+ return ids((await loadPublicationsSchema(root)).statuses);
82
+ } catch {
83
+ const path = join(root, "schemas", "publications.ts");
84
+ const old = await stat(path).then((info) => importProjectModule(path, info.mtimeMs, "schemas/publications.ts")).then((module) => module.default?.groups, () => void 0);
85
+ return Array.isArray(old) ? ids(old) : ids(defaultPublications.statuses);
86
+ }
87
+ }
88
+ async function vueFiles(root, dir = join(root, "app")) {
89
+ const files = [];
90
+ for (const entry of await readdir(dir, { withFileTypes: true }).catch(() => [])) {
91
+ const path = join(dir, entry.name);
92
+ if (entry.isDirectory()) files.push(...await vueFiles(root, path));
93
+ else if (entry.name.endsWith(".vue")) files.push(relative(root, path).split(sep).join("/"));
94
+ }
95
+ return files.sort();
96
+ }
97
+ async function migrateSchema(ws, notes) {
98
+ const path = "schemas/publications.ts";
99
+ const source = await ws.read(path);
100
+ if (source === void 0 || !/\b(groups|defaultGroup)\s*:/.test(source)) return;
101
+ const renamed = renamePublicationsSettings(source, { groups: "statuses", defaultGroup: "defaultStatus" });
102
+ if (!renamed.ok) notes.after.push(`By hand: in ${path}, rename groups to statuses and defaultGroup to defaultStatus (${renamed.reason}).`);
103
+ else if (renamed.value !== source) await ws.edit(path, renamed.value, "groups is now statuses, defaultGroup defaultStatus");
104
+ }
105
+ async function migratePages(root, ws, lists, notes) {
106
+ const statuses = await statusOrder(root);
107
+ for (const path of await vueFiles(root)) {
108
+ const source = await ws.read(path);
109
+ if (/(^|\/)VitaPublications\.vue$/.test(path)) {
110
+ notes.after.push(`By hand: ${path} is your copy of VitaPublications. Its props are now list and title, and it reads usePublicationList(list); \`vitakit eject VitaPublications\` shows vitakit's.`);
111
+ }
112
+ if (/\busePublications\s*\(/.test(source)) {
113
+ notes.after.push(`By hand: ${path} calls usePublications(); use usePublicationList('<list>') with a list from ${LISTS_FILE}.`);
114
+ }
115
+ const found = publicationsElements(source);
116
+ if (!found.ok) {
117
+ if (source.includes("VitaPublications")) notes.after.push(`By hand: ${path} ${found.reason}; check its VitaPublications.`);
118
+ continue;
119
+ }
120
+ const edits = [];
121
+ const shown = [];
122
+ for (const element of found.value) {
123
+ if (element.props.some((prop) => prop.name === "list")) continue;
124
+ const old = element.props.filter((prop) => prop.name === "groups" || prop.name === "headings");
125
+ const legacy = old.length ? legacyList(element, statuses) : statusList();
126
+ if ("reason" in legacy) {
127
+ notes.after.push(`By hand: ${path}:${element.line}: ${legacy.reason}. Declare the list in ${LISTS_FILE} and write <VitaPublications list="<name>" />.`);
128
+ continue;
129
+ }
130
+ const name = lists.name(legacy.wanted, legacy.settings);
131
+ for (const prop of old) {
132
+ let start = prop.start;
133
+ while (start > 0 && /\s/.test(source[start - 1])) start--;
134
+ edits.push({ start, end: prop.end, text: "" });
135
+ }
136
+ const afterTag = element.start + 1 + element.tag.length;
137
+ edits.push({ start: afterTag, end: afterTag, text: ` list="${name}"` });
138
+ shown.push(name);
139
+ if ("note" in legacy && legacy.note) notes.before.push(`${path}:${element.line} shows the list ${name}: ${legacy.note}.`);
140
+ }
141
+ if (!edits.length) continue;
142
+ let result = source;
143
+ for (const edit of edits.sort((a, b) => b.start - a.start)) result = result.slice(0, edit.start) + edit.text + result.slice(edit.end);
144
+ await ws.edit(path, result, `VitaPublications shows ${[...new Set(shown)].join(", ")}`);
145
+ }
146
+ }
147
+ async function migrateDocuments(root, ws, lists, notes) {
148
+ const files = (await readdir(join(root, "cv-tex")).catch(() => [])).filter((name) => name.endsWith(".tex")).sort();
149
+ for (const file of files) {
150
+ const tex = `cv-tex/${file}`;
151
+ const yml = tex.replace(/\.tex$/, ".yml");
152
+ let list = statusList();
153
+ const options = await ws.read(yml);
154
+ const document = options === void 0 ? void 0 : parseDocument(options);
155
+ const pair = document && isMap(document.contents) ? document.contents.items.find((item) => isScalar(item.key) && item.key.value === "publications") : void 0;
156
+ if (options !== void 0 && pair) {
157
+ const groups = document.toJS().publications?.groups;
158
+ if (Array.isArray(groups) && groups.every((group) => typeof group === "string")) list = statusList(groups);
159
+ const start = pair.key.range?.[0];
160
+ const end = pair.value?.range?.[1];
161
+ if (start === void 0 || end === void 0) {
162
+ notes.after.push(`By hand: remove publications from ${yml}; which publications the PDF shows is now a list.`);
163
+ } else {
164
+ const lines2 = options.split("\n");
165
+ const lineOf = (offset) => options.slice(0, offset).split("\n").length;
166
+ lines2.splice(lineOf(start) - 1, lineOf(Math.max(start, end - 1)) - lineOf(start) + 1);
167
+ await ws.edit(yml, lines2.join("\n"), `publications are now a list in ${LISTS_FILE}`);
168
+ }
169
+ }
170
+ const source = await ws.read(tex);
171
+ const inputs = findPdfPlacements("", tex, source).filter((placement) => placement.macro === "vitainput" && placement.section === "publications");
172
+ if (!inputs.length) continue;
173
+ const name = lists.name(list.wanted, list.settings);
174
+ const lines = source.split("\n");
175
+ for (const input of [...inputs].reverse()) {
176
+ const index = input.line - 1;
177
+ const heading = index > 0 ? /^\s*\\section\{([^{}]*)\}\s*$/.exec(lines[index - 1])?.[1] : void 0;
178
+ lines[index] = lines[index].replace("\\vitainput{publications}", `\\vitapublications${heading !== void 0 ? `[${heading}]` : ""}{${name}}`);
179
+ if (heading !== void 0) lines.splice(index - 1, 1);
180
+ }
181
+ await ws.edit(tex, lines.join("\n"), `shows the publication list ${name}`);
182
+ notes.before.push(`${tex} shows the list ${name}: in the PDF too, publications now come in the list's order, as on the website (biblatex sorted them by year and author before), and each group is numbered from 1.`);
183
+ }
184
+ if (await ws.read("cv-tex/templates/publications.eta") !== void 0) {
185
+ notes.after.push("By hand: cv-tex/templates/publications.eta is your copy of the PDF template. vitakit's now cites a list in its order, with `<%~ it.bib.cite() %>` instead of `\\nocite{*}`, inside `\\begin{refcontext}[sorting=none]`.");
186
+ }
187
+ }
188
+ async function migratePublicationLists(root, ws, notes) {
189
+ await migrateSchema(ws, notes);
190
+ const source = await ws.read(LISTS_FILE);
191
+ let existing;
192
+ try {
193
+ existing = source === void 0 ? {} : parse(source) ?? {};
194
+ } catch {
195
+ notes.after.push(`By hand: ${LISTS_FILE} cannot be read; fix it, then run \`vitakit migrate\` again.`);
196
+ return;
197
+ }
198
+ const lists = new ListPlan(typeof existing === "object" && !Array.isArray(existing) ? existing : {});
199
+ await migratePages(root, ws, lists, notes);
200
+ await migrateDocuments(root, ws, lists, notes);
201
+ if (!lists.added.size) return;
202
+ let lines = source ?? LISTS_HEADER;
203
+ for (const [name, settings] of lists.added) {
204
+ const added = addPublicationList(lines, name, settings);
205
+ if (!added.ok) {
206
+ notes.after.push(`By hand: add to ${LISTS_FILE} (${added.reason}):
207
+ ${[...lists.added].map(([other, value]) => publicationListYaml(other, value)).join("\n")}`);
208
+ return;
209
+ }
210
+ lines = added.value;
211
+ }
212
+ notes.after.push(`The new lists are named after what they show. To rename one, change its name in ${LISTS_FILE} and where it is shown.`);
213
+ const reason = `declares ${[...lists.added.keys()].join(", ")}`;
214
+ if (source === void 0) ws.create(LISTS_FILE, lines, reason, { show: true });
215
+ else await ws.edit(LISTS_FILE, lines, reason);
216
+ }
217
+
218
+ export { migratePublicationLists };
@@ -1,7 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readFile, mkdir, writeFile, rename } from 'node:fs/promises';
3
3
  import { join, dirname } from 'node:path';
4
- import { C as CommandError } from '../shared/vitakit.CoWX2Z4R.mjs';
4
+ import { C as CommandError } from '../shared/vitakit.zwr1ssO5.mjs';
5
5
  import '../shared/vitakit.DQ6PmGoL.mjs';
6
6
  import 'node:url';
7
7
  import 'node:process';
@@ -20,13 +20,41 @@ function diff(before, after, context = 2) {
20
20
  endA--;
21
21
  endB--;
22
22
  }
23
- const from = Math.max(0, start - context);
24
- return [
25
- ...a.slice(from, start).map((line) => ` ${line}`),
26
- ...a.slice(start, endA).map((line) => ` - ${line}`),
27
- ...b.slice(start, endB).map((line) => ` + ${line}`),
28
- ...a.slice(endA, Math.min(a.length, endA + context)).map((line) => ` ${line}`)
29
- ];
23
+ const midA = a.slice(start, endA);
24
+ const midB = b.slice(start, endB);
25
+ const ops = a.slice(0, start).map((line) => ({ kind: " ", line }));
26
+ if (midA.length * midB.length > 1e6) {
27
+ ops.push(...midA.map((line) => ({ kind: "-", line })), ...midB.map((line) => ({ kind: "+", line })));
28
+ } else {
29
+ const lengths = Array.from({ length: midA.length + 1 }, () => new Array(midB.length + 1).fill(0));
30
+ for (let i2 = midA.length - 1; i2 >= 0; i2--) {
31
+ for (let j2 = midB.length - 1; j2 >= 0; j2--) {
32
+ lengths[i2][j2] = midA[i2] === midB[j2] ? lengths[i2 + 1][j2 + 1] + 1 : Math.max(lengths[i2 + 1][j2], lengths[i2][j2 + 1]);
33
+ }
34
+ }
35
+ let i = 0;
36
+ let j = 0;
37
+ while (i < midA.length || j < midB.length) {
38
+ if (i < midA.length && j < midB.length && midA[i] === midB[j]) {
39
+ ops.push({ kind: " ", line: midA[i] });
40
+ i++;
41
+ j++;
42
+ } else if (i < midA.length && (j === midB.length || lengths[i + 1][j] >= lengths[i][j + 1])) {
43
+ ops.push({ kind: "-", line: midA[i++] });
44
+ } else {
45
+ ops.push({ kind: "+", line: midB[j++] });
46
+ }
47
+ }
48
+ }
49
+ ops.push(...a.slice(endA).map((line) => ({ kind: " ", line })));
50
+ const shown = ops.map((op, index) => op.kind !== " " || ops.slice(Math.max(0, index - context), index + context + 1).some((near) => near.kind !== " "));
51
+ const lines = [];
52
+ ops.forEach((op, index) => {
53
+ if (!shown[index]) return;
54
+ if (lines.length && !shown[index - 1]) lines.push(" \u2026");
55
+ lines.push(` ${op.kind === " " ? " " : op.kind} ${op.line}`);
56
+ });
57
+ return lines;
30
58
  }
31
59
  function printPlan(io, style, plan) {
32
60
  const lines = [];