@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
package/dist/nuxt.mjs CHANGED
@@ -1,31 +1,34 @@
1
1
  import { defineNuxtModule, createResolver, addTemplate, addTypeTemplate, addImportsDir, addComponentsDir, addPrerenderRoutes, updateTemplates } from '@nuxt/kit';
2
2
  import { createRequire } from 'node:module';
3
- import { l as loadContent } from './shared/vitakit.By5VWQ1n.mjs';
3
+ import { l as loadContent } from './shared/vitakit.DteA9vLy.mjs';
4
+ import { f as fail } from './shared/vitakit.DCK90zq4.mjs';
5
+ import { b as findWebPlacements, p as publicationPlacementProblems } from './shared/vitakit.DvvUYUTS.mjs';
4
6
  import { readdir } from 'node:fs/promises';
5
7
  import { isAbsolute, resolve, relative } from 'node:path';
6
8
  import 'gray-matter';
7
9
  import 'yaml';
10
+ import './chunks/bibliography.mjs';
11
+ import 'node:fs';
12
+ import './shared/vitakit.DQ6PmGoL.mjs';
13
+ import 'node:url';
14
+ import '@behackl/citation-js-extras';
8
15
  import 'unified';
16
+ import 'rehype-parse';
17
+ import 'rehype-sanitize';
18
+ import 'rehype-stringify';
9
19
  import 'remark-parse';
10
20
  import 'remark-gfm';
11
21
  import 'remark-math';
12
22
  import 'remark-rehype';
13
23
  import 'rehype-katex';
14
24
  import 'rehype-raw';
15
- import 'rehype-stringify';
16
25
  import 'unist-util-visit';
17
- import './shared/vitakit.D10HyDhF.mjs';
18
- import 'node:fs';
19
- import './shared/vitakit.DQ6PmGoL.mjs';
20
- import 'node:url';
21
- import '@behackl/citation-js-extras';
22
- import 'rehype-parse';
23
- import 'rehype-sanitize';
24
- import './shared/vitakit.DbSeENVS.mjs';
25
26
  import 'zod';
27
+ import './chunks/publication-lists.mjs';
28
+ import './shared/vitakit.D10HyDhF.mjs';
26
29
  import './chunks/sections.mjs';
27
30
  import './chunks/schemas.mjs';
28
- import './shared/vitakit.BerUaxtF.mjs';
31
+ import './shared/vitakit.DvABWuUc.mjs';
29
32
  import './shared/vitakit.TSMTC5Jl.mjs';
30
33
 
31
34
  function isWithin(directory, path) {
@@ -83,6 +86,11 @@ const module$1 = defineNuxtModule({
83
86
  });
84
87
  let content = await load(!nuxt.options.dev);
85
88
  report(content);
89
+ if (!nuxt.options.dev && !nuxt.options._prepare) {
90
+ const { publications } = await findWebPlacements(rootDir);
91
+ const [problem] = publicationPlacementProblems(publications, [], Object.keys(content.publications));
92
+ if (problem) fail(problem.where, problem.message);
93
+ }
86
94
  nuxt.options.alias["#vitakit/content"] = `${nuxt.options.buildDir}/${TEMPLATE}`;
87
95
  addTemplate({
88
96
  filename: TEMPLATE,
@@ -1,36 +1,35 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Publication groups formatted from `content/publications.bib` at build time.
3
+ * A publication list from `content/publication-lists.yml`, formatted from
4
+ * `content/publications.bib` at build time.
4
5
  *
5
- * This component lays out the groups; each entry is a `VitaPublication`, which
6
- * can be ejected on its own to change how a reference looks. Grouping, style
7
- * and badge fields are configured in `nuxt.config.ts` and
8
- * `content/publications.csl`.
6
+ * Which publications, in what order and in which groups is the list's
7
+ * business; this component decides where it appears and under which heading.
8
+ * Each entry is a `VitaPublication`, which can be ejected on its own to change
9
+ * how a reference looks. Nothing is shown for an empty list, heading included.
9
10
  */
10
11
  import { computed } from 'vue'
11
- import { usePublications } from '#imports'
12
+ import { usePublicationList } from '#imports'
12
13
 
13
- // `withDefaults` is required for the boolean: Vue casts an absent Boolean prop
14
- // to `false`, so `headings` would silently default to off.
15
- const props = withDefaults(defineProps<{
16
- /** Restrict to these group ids, in this order. */
17
- groups?: string[]
18
- /** Show the group label above each list. */
19
- headings?: boolean
20
- }>(), { groups: undefined, headings: true })
14
+ const props = defineProps<{
15
+ /** The list's name in `content/publication-lists.yml`. */
16
+ list: string
17
+ /** A heading above the list. */
18
+ title?: string
19
+ }>()
21
20
 
22
- const all = usePublications()
23
- const shown = computed(() => props.groups
24
- ? props.groups.map(id => all.find(group => group.id === id)).filter(group => group !== undefined)
25
- : all)
21
+ const publications = computed(() => usePublicationList(props.list))
26
22
  </script>
27
23
 
28
24
  <template>
29
- <div class="vita-publications">
30
- <section v-for="group in shown" :key="group.id" class="vita-publications__group">
31
- <h2 v-if="headings" class="vita-publications__heading">
25
+ <div v-if="publications.count" class="vita-publications">
26
+ <h2 v-if="title !== undefined" class="vita-publications__title">
27
+ {{ title }}
28
+ </h2>
29
+ <section v-for="group in publications.groups" :key="group.id" class="vita-publications__group">
30
+ <component :is="title !== undefined ? 'h3' : 'h2'" v-if="group.label !== undefined" class="vita-publications__heading">
32
31
  {{ group.label }}
33
- </h2>
32
+ </component>
34
33
  <ol class="vita-bib csl-bib-body">
35
34
  <VitaPublication v-for="entry in group.entries" :key="entry.key" :entry="entry" />
36
35
  </ol>
@@ -6,7 +6,7 @@
6
6
  * chunk.
7
7
  */
8
8
  import content from '#vitakit/content'
9
- import type { Entry, Page, Profile, PublicationGroup, VitakitContent } from '@anja.codes/vitakit/shared'
9
+ import type { Entry, Page, Profile, PublicationList, VitakitContent } from '@anja.codes/vitakit/shared'
10
10
 
11
11
  export function useVitakit(): VitakitContent {
12
12
  return content
@@ -32,8 +32,19 @@ export function useSectionIds(): string[] {
32
32
  return Object.keys(content.sections)
33
33
  }
34
34
 
35
- export function usePublications(): PublicationGroup[] {
36
- return content.publications
35
+ /**
36
+ * A list declared in `content/publication-lists.yml`, formatted at build time.
37
+ *
38
+ * Unlike a section, an unknown name is an error: a list is declared on
39
+ * purpose, so a name that matches none is a typo. Builds report it before
40
+ * this runs.
41
+ */
42
+ export function usePublicationList(name: string): PublicationList {
43
+ if (Object.hasOwn(content.publications, name)) return content.publications[name]!
44
+ const names = Object.keys(content.publications)
45
+ throw new Error(name
46
+ ? `no publication list "${name}" in content/publication-lists.yml; the lists are ${names.join(', ') || '(none)'}`
47
+ : 'VitaPublications needs a list, e.g. <VitaPublications list="all" />; the lists are in content/publication-lists.yml')
37
48
  }
38
49
 
39
50
  /** The page for a route, or `undefined`. Routes always end with a slash. */
@@ -1,6 +1,7 @@
1
1
  /**
2
- * How `content/publications.bib` is grouped and linked -- on the website, in
3
- * `vitakit verify` and in the PDF alike.
2
+ * The statuses of `content/publications.bib`, and how references are linked
3
+ * -- on the website, in `vitakit verify` and in the PDF alike. Which
4
+ * publications a list shows is decided in `content/publication-lists.yml`.
4
5
  *
5
6
  * Also the file `vitakit customize publications` writes into a project, with the
6
7
  * import rewritten to `@anja.codes/vitakit`; it is therefore the single definition
@@ -11,10 +12,11 @@ import { badgePresets, publications } from '../publications'
11
12
 
12
13
  export default publications({
13
14
  /**
14
- * The values of the `status` field, in the order the groups appear. A
15
- * status that is not listed here is an error, in any capitalisation.
15
+ * The values of the `status` field, with the heading of their group, in
16
+ * the order the groups appear in a list grouped by status. A status that
17
+ * is not listed here is an error, in any capitalisation.
16
18
  */
17
- groups: [
19
+ statuses: [
18
20
  { id: 'published', label: 'Published' },
19
21
  { id: 'accepted', label: 'Accepted' },
20
22
  { id: 'preprint', label: 'Preprints' },
@@ -22,8 +24,8 @@ export default publications({
22
24
  { id: 'other', label: 'Other' },
23
25
  ],
24
26
 
25
- /** The group of an entry without `status`; the first group if left out. */
26
- defaultGroup: 'published',
27
+ /** The status of an entry without one; the first status if left out. */
28
+ defaultStatus: 'published',
27
29
 
28
30
  /**
29
31
  * BibTeX fields that become links after a reference on the website. The
@@ -1,5 +1,5 @@
1
- import { D as DateRange, d as Linked, L as Link, E as Entry } from './vitakit.BfpxDtz1.mjs';
2
- export { C as ContentIssue, b as DatePoint, c as EntryBase, P as Page, e as Profile, f as Publication, g as PublicationBadge, h as PublicationGroup, i as RichText, R as RichTextMarker, V as Visibility, a as VitakitContent } from './vitakit.BfpxDtz1.mjs';
1
+ import { D as DateRange, d as Linked, L as Link, E as Entry } from './vitakit.BfR_mUHm.mjs';
2
+ export { C as ContentIssue, b as DatePoint, c as EntryBase, P as Page, e as Profile, f as Publication, g as PublicationBadge, h as PublicationGroup, i as PublicationList, j as RichText, R as RichTextMarker, V as Visibility, a as VitakitContent } from './vitakit.BfR_mUHm.mjs';
3
3
 
4
4
  /**
5
5
  * Presentation helpers shared by the website and the LaTeX side.
@@ -91,17 +91,28 @@ interface Publication {
91
91
  */
92
92
  fields: Record<string, string>;
93
93
  }
94
- /** One group of formatted references, e.g. all preprints. */
94
+ /** One group of a publication list, e.g. its preprints or its 2024 papers. */
95
95
  interface PublicationGroup {
96
+ /** Made from the list name, `recent-1`; safe as an HTML id and in LaTeX. */
96
97
  id: string;
97
- label: string;
98
+ /** The group's heading; none for the single group of an ungrouped list. */
99
+ label?: string;
98
100
  count: number;
99
101
  /**
100
- * Newest first. Formatted together, so numbering and year suffixes
102
+ * In the list's order. Formatted together, so numbering and year suffixes
101
103
  * (2024a, 2024b) are right within the group.
102
104
  */
103
105
  entries: Publication[];
104
106
  }
107
+ /**
108
+ * A list declared in `content/publication-lists.yml`, selected, sorted,
109
+ * grouped and formatted at build time. An ungrouped list is one group
110
+ * without a label.
111
+ */
112
+ interface PublicationList {
113
+ count: number;
114
+ groups: PublicationGroup[];
115
+ }
105
116
  interface Page {
106
117
  /** Route path, with leading and trailing slash: `/`, `/research/`. */
107
118
  route: string;
@@ -146,9 +157,10 @@ interface VitakitContent {
146
157
  pages: Page[];
147
158
  /** Section id -> validated, sorted, visibility-filtered entries. */
148
159
  sections: Record<string, Entry[]>;
149
- publications: PublicationGroup[];
160
+ /** List name -> its publications, as `content/publication-lists.yml` declares. */
161
+ publications: Record<string, PublicationList>;
150
162
  /** Always empty in a production build, which fails instead. */
151
163
  issues: ContentIssue[];
152
164
  }
153
165
 
154
- export type { ContentIssue as C, DateRange as D, Entry as E, Link as L, Page as P, RichTextMarker as R, Visibility as V, VitakitContent as a, DatePoint as b, EntryBase as c, Linked as d, Profile as e, Publication as f, PublicationBadge as g, PublicationGroup as h, RichText as i };
166
+ export type { ContentIssue as C, DateRange as D, Entry as E, Link as L, Page as P, RichTextMarker as R, Visibility as V, VitakitContent as a, DatePoint as b, EntryBase as c, Linked as d, Profile as e, Publication as f, PublicationBadge as g, PublicationGroup as h, PublicationList as i, RichText as j };
@@ -175,5 +175,25 @@ function ensurePackageImports(source, names) {
175
175
  const position = end === -1 ? source.length : end + 1;
176
176
  return { ok: true, value: `${source.slice(0, position)}${end === -1 ? "\n" : ""}${line}${source.slice(position)}` };
177
177
  }
178
+ function renamePublicationsSettings(source, renames) {
179
+ let body;
180
+ try {
181
+ body = parseModule(source).body;
182
+ } catch (error) {
183
+ return { ok: false, reason: `cannot be parsed: ${error instanceof Error ? error.message : String(error)}` };
184
+ }
185
+ const expression = defaultExport(body);
186
+ const object = expression?.type === "CallExpression" && expression.callee.type === "Identifier" && packageImports(body).get(expression.callee.name) === "publications" ? expression.arguments[0] : void 0;
187
+ if (!object || object.type !== "ObjectExpression") {
188
+ return { ok: false, reason: `its default export is not publications({ ... }) from ${PACKAGE}` };
189
+ }
190
+ let result = source;
191
+ for (const property of [...object.properties].reverse()) {
192
+ const name = keyName(property);
193
+ if (name === void 0 || !Object.hasOwn(renames, name) || property.type !== "ObjectProperty") continue;
194
+ result = result.slice(0, property.key.start) + renames[name] + result.slice(property.key.end);
195
+ }
196
+ return { ok: true, value: result };
197
+ }
178
198
 
179
- export { addSchemaField as a, ensurePackageImports as e, propertyKey as p };
199
+ export { addSchemaField as a, ensurePackageImports as e, propertyKey as p, renamePublicationsSettings as r };
@@ -28,7 +28,16 @@ function importFailure(error, node = process.versions.node) {
28
28
  if (error?.code === "ERR_UNKNOWN_FILE_EXTENSION") {
29
29
  return `cannot load TypeScript on Node ${node}; vitakit needs Node 22.18 or newer`;
30
30
  }
31
- return messageOf(error);
31
+ const message = messageOf(error);
32
+ const stack = error instanceof Error ? error.stack ?? "" : "";
33
+ const syntax = /^[^\n]*:(\d+)\n((?:[^\n]*\n)*?[ \t]*\^+)[ \t]*\n/.exec(stack);
34
+ if (syntax) {
35
+ const hint = /^Expected ','/.test(message) ? " (often a comma missing before it)" : "";
36
+ return `line ${syntax[1]}: ${message}${hint}
37
+ ${syntax[2].split("\n").map((line2) => ` ${line2}`).join("\n")}`;
38
+ }
39
+ const line = /\.ts(?:\?[^:\s)]*)?:(\d+):\d+/.exec(stack)?.[1];
40
+ return line ? `line ${line}: ${message}` : message;
32
41
  }
33
42
  function formatZodIssues(error) {
34
43
  return error.issues.map((issue) => ` - ${issue.path.join(".") || "(root)"}: ${issue.message}`).join("\n");
@@ -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) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;" })[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 };
@@ -1,5 +1,5 @@
1
1
  import { z } from 'zod';
2
- import { f as fail } from './vitakit.DbSeENVS.mjs';
2
+ import { f as fail } from './vitakit.DCK90zq4.mjs';
3
3
  import { i as isSitePath, s as sitePathMarker, P as PATH_KEY } from './vitakit.D10HyDhF.mjs';
4
4
 
5
5
  const MARKER_KEY = "__vitakitRich";
@@ -52,6 +52,7 @@ async function findWebPlacements(rootDir) {
52
52
  components.set(componentName(relative(componentsDir, file)), posix(relative(rootDir, file)));
53
53
  }
54
54
  const placements = [];
55
+ const publications = [];
55
56
  const dynamic = [];
56
57
  for (const path of await vueFiles(appDir)) {
57
58
  const file = posix(relative(rootDir, path));
@@ -63,7 +64,19 @@ async function findWebPlacements(rootDir) {
63
64
  const visit = (node) => {
64
65
  if (node.type === ELEMENT && node.tag) {
65
66
  const tag = node.tag.includes("-") ? pascal(node.tag) : node.tag;
66
- if (tag === "VitaSection" || components.has(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)) {
67
80
  const id = attribute(node, "id");
68
81
  const section = id?.static ?? (id?.expression !== void 0 ? literal(id.expression) : void 0);
69
82
  if (section) {
@@ -99,7 +112,37 @@ async function findWebPlacements(rootDir) {
99
112
  }
100
113
  }
101
114
  }
102
- return { placements, dynamic, components };
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;
103
146
  }
104
147
  function stripComment(line) {
105
148
  const match = /(^|[^\\])%/.exec(line);
@@ -131,6 +174,21 @@ function findPdfPlacements(document, file, source) {
131
174
  }
132
175
  return placements;
133
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
+ }
134
192
  function documentDirective(source) {
135
193
  for (const [index, raw] of source.split("\n").entries()) {
136
194
  const match = /\\vitadocument\{([^}]*)\}/.exec(stripComment(raw));
@@ -139,4 +197,4 @@ function documentDirective(source) {
139
197
  return void 0;
140
198
  }
141
199
 
142
- export { findWebPlacements as a, documentDirective as d, findPdfPlacements as f, pageRoute as p };
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 };