@anja.codes/vitakit 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +52 -131
  2. package/dist/check.mjs +20 -11
  3. package/dist/chunks/authoring.mjs +372 -0
  4. package/dist/{shared/vitakit.CypCF-4Q.mjs → chunks/bibliography.mjs} +74 -237
  5. package/dist/chunks/build.mjs +178 -0
  6. package/dist/chunks/documents.mjs +152 -0
  7. package/dist/chunks/draft.mjs +222 -0
  8. package/dist/chunks/eject.mjs +59 -0
  9. package/dist/chunks/emit.mjs +216 -0
  10. package/dist/chunks/eta.mjs +16 -0
  11. package/dist/chunks/fields.mjs +79 -0
  12. package/dist/chunks/inventory.mjs +201 -0
  13. package/dist/chunks/migrations.mjs +218 -0
  14. package/dist/chunks/plan.mjs +172 -0
  15. package/dist/chunks/prompt.mjs +33 -0
  16. package/dist/chunks/publication-lists.mjs +202 -0
  17. package/dist/chunks/schema-source.mjs +12 -0
  18. package/dist/chunks/schemas.mjs +109 -0
  19. package/dist/chunks/sections.mjs +109 -0
  20. package/dist/chunks/snippets.mjs +199 -0
  21. package/dist/chunks/validate.mjs +133 -0
  22. package/dist/chunks/vue.mjs +153 -0
  23. package/dist/chunks/yaml.mjs +62 -0
  24. package/dist/cli.d.mts +33 -40
  25. package/dist/cli.mjs +7 -224
  26. package/dist/index.d.mts +379 -49
  27. package/dist/index.mjs +15 -7
  28. package/dist/nuxt.mjs +48 -10
  29. package/dist/runtime/components/VitaPublications.vue +21 -22
  30. package/dist/runtime/components/VitaSection.vue +94 -38
  31. package/dist/runtime/composables/useVitakit.ts +14 -3
  32. package/dist/schemas/education.ts +4 -4
  33. package/dist/schemas/employment.ts +3 -3
  34. package/dist/schemas/publications.ts +10 -8
  35. package/dist/schemas/supervision.ts +3 -3
  36. package/dist/schemas/talks.ts +1 -1
  37. package/dist/schemas/teaching.ts +3 -3
  38. package/dist/shared/index.d.mts +60 -9
  39. package/dist/shared/index.mjs +3 -57
  40. package/dist/shared/{vitakit.D8WRg_Xx.d.mts → vitakit.BfR_mUHm.d.mts} +25 -12
  41. package/dist/shared/vitakit.CCTDg7pz.mjs +199 -0
  42. package/dist/shared/vitakit.CfB3hMHl.mjs +60 -0
  43. package/dist/shared/{vitakit.BlU-p01e.mjs → vitakit.CtChKO0I.mjs} +1 -1
  44. package/dist/shared/vitakit.D-DdJaDZ.mjs +20 -0
  45. package/dist/shared/vitakit.D10HyDhF.mjs +19 -0
  46. package/dist/shared/vitakit.DCK90zq4.mjs +67 -0
  47. package/dist/shared/vitakit.DQ6PmGoL.mjs +34 -0
  48. package/dist/shared/vitakit.DcQOdM4I.mjs +57 -0
  49. package/dist/shared/vitakit.DteA9vLy.mjs +135 -0
  50. package/dist/shared/vitakit.DvABWuUc.mjs +121 -0
  51. package/dist/shared/vitakit.DvvUYUTS.mjs +200 -0
  52. package/dist/shared/vitakit.TSMTC5Jl.mjs +6 -0
  53. package/dist/shared/vitakit.UfoXdL7U.mjs +22 -0
  54. package/dist/shared/vitakit.zwr1ssO5.mjs +1850 -0
  55. package/dist/tex/templates/_generic.eta +6 -4
  56. package/dist/tex/templates/education.eta +10 -5
  57. package/dist/tex/templates/employment.eta +12 -5
  58. package/dist/tex/templates/publications.eta +7 -3
  59. package/dist/tex/templates/supervision.eta +10 -2
  60. package/dist/tex/templates/talks.eta +5 -1
  61. package/dist/tex/templates/teaching.eta +6 -4
  62. package/dist/tex/vitakit-cv.cls +36 -10
  63. package/dist/tex.d.mts +80 -11
  64. package/dist/tex.mjs +27 -117
  65. package/package.json +10 -7
  66. package/src/bin/vitakit.mjs +3 -153
  67. package/dist/shared/vitakit.BRhjqpAt.mjs +0 -250
  68. package/dist/shared/vitakit.BxcnlFGP.mjs +0 -325
  69. package/src/bin/vitakit-build-cv.mjs +0 -50
  70. package/src/bin/vitakit-check.mjs +0 -57
package/dist/nuxt.mjs CHANGED
@@ -1,26 +1,58 @@
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.CypCF-4Q.mjs';
4
- import 'node:fs/promises';
5
- import 'node:path';
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';
6
+ import { readdir } from 'node:fs/promises';
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.BxcnlFGP.mjs';
18
- import 'node:fs';
19
- import 'node:url';
20
26
  import 'zod';
21
- import '@behackl/citation-js-extras';
22
- import 'rehype-parse';
23
- import 'rehype-sanitize';
27
+ import './chunks/publication-lists.mjs';
28
+ import './shared/vitakit.D10HyDhF.mjs';
29
+ import './chunks/sections.mjs';
30
+ import './chunks/schemas.mjs';
31
+ import './shared/vitakit.DvABWuUc.mjs';
32
+ import './shared/vitakit.TSMTC5Jl.mjs';
33
+
34
+ function isWithin(directory, path) {
35
+ const child = relative(directory, path);
36
+ return child === "" || !isAbsolute(child) && child.split(/[/\\]/)[0] !== "..";
37
+ }
38
+ async function watchComponentOverrides(nuxt, packagedDir) {
39
+ const names = new Set((await readdir(packagedDir)).filter((file) => file.endsWith(".vue")).map((file) => file.slice(0, -4)));
40
+ let previous;
41
+ let restarting = false;
42
+ nuxt.hook("components:extend", async (components) => {
43
+ const overrides = new Set(components.filter((component) => names.has(component.pascalName) && isAbsolute(component.filePath) && !isWithin(packagedDir, component.filePath) && !component.filePath.replace(/\\/g, "/").split("/").includes("node_modules") && (isWithin(nuxt.options.srcDir, component.filePath) || isWithin(nuxt.options.rootDir, component.filePath))).map((component) => resolve(component.filePath)));
44
+ const before = previous;
45
+ previous = overrides;
46
+ if (!before || restarting || before.size === overrides.size && [...before].every((path) => overrides.has(path))) return;
47
+ restarting = true;
48
+ try {
49
+ await nuxt.callHook("restart");
50
+ } catch (error) {
51
+ restarting = false;
52
+ throw error;
53
+ }
54
+ });
55
+ }
24
56
 
25
57
  function katexStylesheet() {
26
58
  const renderer = createRequire(createRequire(import.meta.url).resolve("rehype-katex"));
@@ -54,6 +86,11 @@ const module$1 = defineNuxtModule({
54
86
  });
55
87
  let content = await load(!nuxt.options.dev);
56
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
+ }
57
94
  nuxt.options.alias["#vitakit/content"] = `${nuxt.options.buildDir}/${TEMPLATE}`;
58
95
  addTemplate({
59
96
  filename: TEMPLATE,
@@ -78,6 +115,7 @@ const module$1 = defineNuxtModule({
78
115
  await addComponentsDir({ path: resolver.resolve("./runtime/components"), prefix: "Vita", pathPrefix: false });
79
116
  addPrerenderRoutes(content.pages.map((page) => page.route));
80
117
  if (!nuxt.options.dev) return;
118
+ await watchComponentOverrides(nuxt, resolver.resolve("./runtime/components"));
81
119
  nuxt.options.watch.push("../content", "../schemas");
82
120
  nuxt.hook("builder:watch", async (_event, path) => {
83
121
  if (!/[/\\](content|schemas)[/\\]/.test(path)) return;
@@ -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>
@@ -1,15 +1,26 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Renders any section without a dedicated component.
3
+ * A generic section with conventional field selection and optional overrides.
4
4
  *
5
- * It shows the fields a vitakit schema is built from -- a date, something
6
- * title-like, an institution, a note, a link -- and ignores what it does not
7
- * understand. That is enough to make a newly added `content/<name>.yml` visible
8
- * immediately; write your own component (or eject one) when you want control.
5
+ * title/meta/note slots replace those regions; the default slot replaces the
6
+ * whole entry body. The list, row and date column remain owned by vitakit.
7
+ *
8
+ * With `title`, the list comes with its heading, in a <section> carrying the
9
+ * section id as anchor, and neither appears when there are no entries -- the
10
+ * website's `\vitasection[Title]{id}`.
9
11
  */
10
- import { computed } from 'vue'
12
+ import { computed, type VNodeChild } from 'vue'
11
13
  import { useSection } from '#imports'
12
- import { entrySecondaryParts, entryTitleField, richHtml, type Entry } from '@anja.codes/vitakit/shared'
14
+ import {
15
+ entrySecondaryParts,
16
+ entryTitleField,
17
+ filterWhere,
18
+ richHtml,
19
+ type DateRange,
20
+ type Entry,
21
+ type VitaSectionSlotProps,
22
+ type Where,
23
+ } from '@anja.codes/vitakit/shared'
13
24
 
14
25
  const props = defineProps<{
15
26
  /** Section id; ignored when `entries` is given. */
@@ -17,44 +28,89 @@ const props = defineProps<{
17
28
  entries?: Entry[]
18
29
  /** Fields to try, in order, for the main line. */
19
30
  titleFields?: string[]
31
+ /** Only entries whose fields have these values, as `where` in a PDF document. */
32
+ where?: Where
20
33
  limit?: number
34
+ /** A heading; the list is then wrapped in a <section> with the id as anchor. */
35
+ title?: string
21
36
  }>()
22
37
 
23
- const items = computed(() => {
24
- const source = props.entries ?? (props.id ? useSection(props.id) : [])
25
- return props.limit ? source.slice(0, props.limit) : source
26
- })
27
-
28
- // Which fields are the headline and the context line is decided in
29
- // `@anja.codes/vitakit/shared`, the same code the LaTeX emitter uses; only the
30
- // separator and the markup are decided here.
31
- const titleFieldOf = (entry: Entry): string | undefined =>
32
- entryTitleField(entry, props.titleFields)
33
-
34
- const titleOf = (entry: Entry): string => {
35
- const field = titleFieldOf(entry)
36
- return (field && richHtml(entry[field])) || ''
37
- }
38
-
39
- const secondaryOf = (entry: Entry): string =>
40
- entrySecondaryParts(entry, titleFieldOf(entry))
41
- .map(part => (part.kind === 'institution' ? part.institution.name : part.text))
42
- .join(' · ')
38
+ const slots = defineSlots<{
39
+ title?(props: VitaSectionSlotProps): VNodeChild
40
+ meta?(props: VitaSectionSlotProps): VNodeChild
41
+ note?(props: VitaSectionSlotProps): VNodeChild
42
+ default?(props: VitaSectionSlotProps): VNodeChild
43
+ }>()
43
44
 
44
- const linkOf = (entry: Entry): string | undefined =>
45
- typeof entry.url === 'string' ? entry.url : undefined
45
+ const rows = computed(() => {
46
+ const source = filterWhere(props.entries ?? (props.id ? useSection(props.id) : []), props.where)
47
+ const items = props.limit ? source.slice(0, props.limit) : source
48
+ return items.map((entry, index): VitaSectionSlotProps => {
49
+ // Field selection remains shared with the emitter; only the web-specific
50
+ // presentation (such as a term replacing the date) is decided here.
51
+ const titleField = entryTitleField(entry, props.titleFields)
52
+ const term = typeof entry.term === 'string' && entry.term.length > 0 ? entry.term : undefined
53
+ return {
54
+ entry,
55
+ index,
56
+ display: {
57
+ titleHtml: (titleField && richHtml(entry[titleField])) || '',
58
+ titleUrl: typeof entry.url === 'string' ? entry.url : undefined,
59
+ date: entry.date as DateRange | undefined,
60
+ term,
61
+ meta: entrySecondaryParts(term ? { ...entry, term: undefined } : entry, titleField),
62
+ note: typeof entry.note === 'string' ? entry.note : undefined,
63
+ links: typeof entry.slides === 'string' && entry.slides ? [{ label: 'Slides', url: entry.slides }] : [],
64
+ ongoing: entry.ongoing === true,
65
+ },
66
+ }
67
+ })
68
+ })
46
69
  </script>
47
70
 
48
71
  <template>
49
- <ul v-if="items.length" class="vita-section">
50
- <VitaEntry v-for="item in items" :key="item.id" :date="item.date as never">
51
- <p class="vita-entry__title">
52
- <!-- eslint-disable vue/no-v-html -- build-time rendered Markdown -->
53
- <a v-if="linkOf(item)" :href="linkOf(item)" v-html="titleOf(item)" />
54
- <span v-else v-html="titleOf(item)" />
55
- </p>
56
- <p v-if="secondaryOf(item)" class="vita-entry__meta">{{ secondaryOf(item) }}</p>
57
- <p v-if="typeof item.note === 'string'" class="vita-entry__note">{{ item.note }}</p>
72
+ <!-- With a title: the same component without one, inside its section. -->
73
+ <section v-if="title !== undefined && rows.length" :id="id" class="vita-section-group">
74
+ <h2 class="vita-section-group__title">
75
+ {{ title }}
76
+ </h2>
77
+ <VitaSection :entries="rows.map(row => row.entry)" :title-fields="titleFields">
78
+ <template v-for="(_, name) in slots" #[name]="scope">
79
+ <slot :name="name" v-bind="scope" />
80
+ </template>
81
+ </VitaSection>
82
+ </section>
83
+ <ul v-else-if="title === undefined && rows.length" class="vita-section">
84
+ <VitaEntry v-for="row in rows" :key="row.entry.id" :date="row.display.date">
85
+ <template v-if="row.display.term" #date>{{ row.display.term }}</template>
86
+ <!-- Check presence explicitly: an intentionally empty slot must not
87
+ bring back Vue's slot fallback (or an empty default wrapper). -->
88
+ <slot v-if="slots.default" v-bind="row" />
89
+ <template v-else>
90
+ <slot v-if="slots.title" name="title" v-bind="row" />
91
+ <p v-else class="vita-entry__title">
92
+ <!-- eslint-disable vue/no-v-html -- build-time rendered Markdown -->
93
+ <a v-if="row.display.titleUrl" :href="row.display.titleUrl" v-html="row.display.titleHtml" />
94
+ <span v-else v-html="row.display.titleHtml" />
95
+ <span v-if="row.display.ongoing" class="vita-entry__status"> (in progress)</span>
96
+ </p>
97
+ <slot v-if="slots.meta" name="meta" v-bind="row" />
98
+ <p v-else-if="row.display.meta.length" class="vita-entry__meta">
99
+ <template v-for="(part, index) in row.display.meta" :key="index">
100
+ <template v-if="index"> · </template>
101
+ <template v-if="part.kind === 'linked'">
102
+ <a v-if="part.linked.url" :href="part.linked.url">{{ part.linked.label }}</a>
103
+ <template v-else>{{ part.linked.label }}</template>
104
+ </template>
105
+ <template v-else>{{ part.text }}</template>
106
+ </template>
107
+ </p>
108
+ <slot v-if="slots.note" name="note" v-bind="row" />
109
+ <p v-else-if="row.display.note !== undefined" class="vita-entry__note">{{ row.display.note }}</p>
110
+ <p v-if="row.display.links.length" class="vita-entry__links">
111
+ <a v-for="link in row.display.links" :key="link.url" :href="link.url">{{ link.label }}</a>
112
+ </p>
113
+ </template>
58
114
  </VitaEntry>
59
115
  </ul>
60
116
  </template>
@@ -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,20 +1,20 @@
1
1
  /**
2
2
  * Shape of `content/education.yml`.
3
3
  *
4
- * Also the file `vitakit eject education` copies into a project, with the import
4
+ * Also the file `vitakit customize education --schema --copy` writes into a project, with the import
5
5
  * rewritten to `@anja.codes/vitakit`; it is therefore the single definition of
6
6
  * this section, not a copy of one.
7
7
  */
8
- import { dateRange, institution, link, richText, section, text, url, z } from '../primitives'
8
+ import { dateRange, linked, richText, section, text, url, z } from '../primitives'
9
9
 
10
10
  export default section({
11
11
  date: dateRange,
12
12
  degree: text,
13
- institution,
13
+ institution: linked,
14
14
  thesis: z.strictObject({
15
15
  title: richText,
16
16
  type: text.optional(),
17
- supervisor: z.union([text, link]).optional(),
17
+ supervisor: linked.optional(),
18
18
  url: url.optional(),
19
19
  }).optional(),
20
20
  note: text.optional(),
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * Shape of `content/employment.yml`.
3
3
  *
4
- * Also the file `vitakit eject employment` copies into a project, with the import
4
+ * Also the file `vitakit customize employment --schema --copy` writes into a project, with the import
5
5
  * rewritten to `@anja.codes/vitakit`; it is therefore the single definition of
6
6
  * this section, not a copy of one.
7
7
  */
8
- import { dateRange, institution, richText, section, text, url, z } from '../primitives'
8
+ import { dateRange, linked, richText, section, text, url, z } from '../primitives'
9
9
 
10
10
  export default section({
11
11
  date: dateRange,
12
12
  position: text,
13
- institution,
13
+ institution: linked,
14
14
  project: z.strictObject({
15
15
  title: richText,
16
16
  funder: text.optional(),
@@ -1,8 +1,9 @@
1
1
  /**
2
- * How `content/publications.bib` is grouped and linked -- on the website, in
3
- * `vitakit-check` 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
- * Also the file `vitakit eject publications` copies into a project, with the
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
7
8
  * of the defaults, not a copy of them. Every setting is optional: leave out
8
9
  * what you do not change.
@@ -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,11 +1,11 @@
1
1
  /**
2
2
  * Shape of `content/supervision.yml`.
3
3
  *
4
- * Also the file `vitakit eject supervision` copies into a project, with the import
4
+ * Also the file `vitakit customize supervision --schema --copy` writes into a project, with the import
5
5
  * rewritten to `@anja.codes/vitakit`; it is therefore the single definition of
6
6
  * this section, not a copy of one.
7
7
  */
8
- import { dateRange, institution, richText, section, text, z } from '../primitives'
8
+ import { dateRange, linked, richText, section, text, z } from '../primitives'
9
9
 
10
10
  export default section({
11
11
  date: dateRange,
@@ -15,6 +15,6 @@ export default section({
15
15
  /** Markdown, math allowed. */
16
16
  title: richText,
17
17
  degree: text.optional(),
18
- institution: institution.optional(),
18
+ institution: linked.optional(),
19
19
  coSupervisor: text.optional(),
20
20
  })
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Shape of `content/talks.yml`.
3
3
  *
4
- * Also the file `vitakit eject talks` copies into a project, with the import
4
+ * Also the file `vitakit customize talks --schema --copy` writes into a project, with the import
5
5
  * rewritten to `@anja.codes/vitakit`; it is therefore the single definition of
6
6
  * this section, not a copy of one.
7
7
  */
@@ -1,17 +1,17 @@
1
1
  /**
2
2
  * Shape of `content/teaching.yml`.
3
3
  *
4
- * Also the file `vitakit eject teaching` copies into a project, with the import
4
+ * Also the file `vitakit customize teaching --schema --copy` writes into a project, with the import
5
5
  * rewritten to `@anja.codes/vitakit`; it is therefore the single definition of
6
6
  * this section, not a copy of one.
7
7
  */
8
- import { dateRange, institution, richText, section, text, url } from '../primitives'
8
+ import { dateRange, linked, richText, section, text, url } from '../primitives'
9
9
 
10
10
  export default section({
11
11
  date: dateRange,
12
12
  title: richText,
13
13
  role: text.optional(),
14
- institution: institution.optional(),
14
+ institution: linked.optional(),
15
15
  /** e.g. `2024S`, `Winter 2023/24` -- free text, shown as written. */
16
16
  term: text.optional(),
17
17
  url: url.optional(),
@@ -1,5 +1,5 @@
1
- import { D as DateRange, I as Institution, E as Entry } from './vitakit.D8WRg_Xx.mjs';
2
- export { C as ContentIssue, b as DatePoint, c as EntryBase, L as Link, P as Page, d as Profile, e as Publication, f as PublicationBadge, g as PublicationGroup, h as RichText, R as RichTextMarker, V as Visibility, a as VitakitContent } from './vitakit.D8WRg_Xx.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.
@@ -37,32 +37,83 @@ declare function dateSortKey(range: DateRange | undefined): string;
37
37
  declare const TITLE_FIELDS: readonly string[];
38
38
  /** Plain-text fields tried, in order, for the context line. */
39
39
  declare const SECONDARY_FIELDS: readonly string[];
40
- /** Fields that may hold an institution, in order of preference. */
41
- declare const INSTITUTION_FIELDS: readonly string[];
40
+ /** `linked` fields tried, in order, for the place on the context line. */
41
+ declare const LINKED_FIELDS: readonly string[];
42
42
  /**
43
43
  * The field holding the entry's headline, or `undefined` when nothing fits.
44
44
  * Returns the field *name* so callers can render its value themselves: the
45
45
  * website needs the rendered HTML, the emitter the LaTeX source.
46
46
  */
47
47
  declare function entryTitleField(entry: Entry, fields?: readonly string[]): string | undefined;
48
- /** A piece of the context line: plain text, or an institution that may link. */
48
+ /** A piece of the context line: plain text, or a `linked` value that may link. */
49
49
  type SecondaryPart = {
50
50
  kind: 'text';
51
51
  text: string;
52
52
  } | {
53
- kind: 'institution';
54
- institution: Institution;
53
+ kind: 'linked';
54
+ linked: Linked;
55
55
  };
56
+ /** Values used by VitaSection's default web rendering, also available to slots. */
57
+ interface VitaSectionDisplay {
58
+ /** Build-time rendered HTML, respecting the component's titleFields. */
59
+ titleHtml: string;
60
+ titleUrl?: string;
61
+ date?: DateRange;
62
+ /** A teaching term replaces the normal date label. */
63
+ term?: string;
64
+ /** Structured context, preserving links. */
65
+ meta: SecondaryPart[];
66
+ note?: string;
67
+ /** Additional links; does not include the title's link. */
68
+ links: Link[];
69
+ ongoing: boolean;
70
+ }
71
+ /** Shared context for VitaSection's title, meta, note and default (body) slots. */
72
+ interface VitaSectionSlotProps {
73
+ entry: Entry;
74
+ /** Zero-based position in the displayed list, after limiting. */
75
+ index: number;
76
+ display: VitaSectionDisplay;
77
+ }
56
78
  /**
57
79
  * The context line, as structured parts. The field used for the title is
58
80
  * skipped, so nothing appears twice.
59
81
  */
60
82
  declare function entrySecondaryParts(entry: Entry, titleField?: string): SecondaryPart[];
83
+ /**
84
+ * The `linked` values of a field, as a list: none, one, or several. For
85
+ * templates that show a field which may hold one link or a list of them.
86
+ */
87
+ declare function asLinkedList(value: unknown): Linked[];
61
88
 
62
89
  /** The plain-text form: Markdown source, for LaTeX, comparisons and `title=`. */
63
90
  declare function richSource(value: unknown): string;
64
91
  /** The rendered form, or `undefined` when there is nothing to render. */
65
92
  declare function richHtml(value: unknown): string | undefined;
66
93
 
67
- export { DateRange, Entry, INSTITUTION_FIELDS, Institution, SECONDARY_FIELDS, TITLE_FIELDS, dateSortKey, entrySecondaryParts, entryTitleField, formatDateRange, richHtml, richSource };
68
- export type { DateFormatOptions, SecondaryPart };
94
+ /**
95
+ * Selecting entries by field: the `where` of a PDF document's `.yml` and of
96
+ * `<VitaSection where>`. One implementation, so that the website and the PDF
97
+ * cannot disagree about which entries match.
98
+ *
99
+ * Deliberately small: equality with one value, or with any of several. Values
100
+ * are compared as text, so `where: { year: 2024 }` and `{ year: '2024' }` mean
101
+ * the same.
102
+ */
103
+
104
+ /** A value to compare with, or several of which any may match. */
105
+ type WhereValue = string | number | boolean | readonly (string | number | boolean)[];
106
+ /** Field name -> wanted value(s). Every field must match. */
107
+ type Where = Readonly<Record<string, WhereValue>>;
108
+ /**
109
+ * The comparable value of a field: `year` from the date, the source text of a
110
+ * formatted-text field, the label of a link.
111
+ */
112
+ declare function fieldValue(entry: Entry, field: string): unknown;
113
+ /** Whether an entry matches every field of `where`. */
114
+ declare function matchesWhere(entry: Entry, where: Where): boolean;
115
+ /** The entries matching `where`; all of them when it is empty or absent. */
116
+ declare function filterWhere<T extends Entry>(entries: readonly T[], where: Where | undefined): T[];
117
+
118
+ export { DateRange, Entry, LINKED_FIELDS, Link, Linked, SECONDARY_FIELDS, TITLE_FIELDS, asLinkedList, dateSortKey, entrySecondaryParts, entryTitleField, fieldValue, filterWhere, formatDateRange, matchesWhere, richHtml, richSource };
119
+ export type { DateFormatOptions, SecondaryPart, VitaSectionDisplay, VitaSectionSlotProps, Where, WhereValue };
@@ -1,57 +1,3 @@
1
- export { a as richHtml, r as richSource } from './vitakit.BlU-p01e.mjs';
2
-
3
- function formatPoint(point, { locale = "en", precision = "auto" }) {
4
- if (precision === "year" || point.month === void 0) return String(point.year);
5
- const date = new Date(Date.UTC(point.year, point.month - 1, point.day ?? 1));
6
- return new Intl.DateTimeFormat(locale, {
7
- year: "numeric",
8
- month: "short",
9
- ...point.day === void 0 ? {} : { day: "numeric" },
10
- timeZone: "UTC"
11
- }).format(date);
12
- }
13
- function formatDateRange(range, options = {}) {
14
- if (!range) return "";
15
- const { presentLabel = "present" } = options;
16
- const from = formatPoint(range.from, options);
17
- if (range.point) return from;
18
- if (range.ongoing) return `${from}\u2013${presentLabel}`;
19
- if (!range.to) return from;
20
- const to = formatPoint(range.to, options);
21
- return from === to ? from : `${from}\u2013${to}`;
22
- }
23
- function dateSortKey(range) {
24
- return range?.from.sortKey ?? "";
25
- }
26
-
27
- const TITLE_FIELDS = ["title", "position", "degree", "role", "student"];
28
- const SECONDARY_FIELDS = ["student", "role", "event", "term", "degree"];
29
- const INSTITUTION_FIELDS = ["institution", "issuer", "organisation"];
30
- function entryTitleField(entry, fields = TITLE_FIELDS) {
31
- return fields.find((field) => {
32
- const value = entry[field];
33
- if (typeof value === "string") return value.length > 0;
34
- return typeof value === "object" && value !== null && "html" in value;
35
- });
36
- }
37
- function entrySecondaryParts(entry, titleField) {
38
- const parts = [];
39
- for (const field of SECONDARY_FIELDS) {
40
- if (field === titleField) continue;
41
- const value = entry[field];
42
- if (typeof value === "string" && value.length > 0) parts.push({ kind: "text", text: value });
43
- }
44
- for (const field of INSTITUTION_FIELDS) {
45
- const value = entry[field];
46
- if (value?.name) {
47
- parts.push({ kind: "institution", institution: value });
48
- break;
49
- }
50
- }
51
- if (typeof entry.location === "string" && entry.location.length > 0) {
52
- parts.push({ kind: "text", text: entry.location });
53
- }
54
- return parts;
55
- }
56
-
57
- export { INSTITUTION_FIELDS, SECONDARY_FIELDS, TITLE_FIELDS, dateSortKey, entrySecondaryParts, entryTitleField, formatDateRange };
1
+ export { L as LINKED_FIELDS, S as SECONDARY_FIELDS, T as TITLE_FIELDS, a as asLinkedList, d as dateSortKey, e as entrySecondaryParts, b as entryTitleField, f as formatDateRange } from './vitakit.CfB3hMHl.mjs';
2
+ export { r as richHtml, a as richSource } from './vitakit.CtChKO0I.mjs';
3
+ export { f as fieldValue, a as filterWhere, m as matchesWhere } from './vitakit.D-DdJaDZ.mjs';