@anja.codes/vitakit 0.1.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 (48) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +159 -0
  3. package/dist/check.d.mts +22 -0
  4. package/dist/check.mjs +196 -0
  5. package/dist/cli.d.mts +45 -0
  6. package/dist/cli.mjs +225 -0
  7. package/dist/index.d.mts +398 -0
  8. package/dist/index.mjs +21 -0
  9. package/dist/nuxt.d.mts +15 -0
  10. package/dist/nuxt.mjs +98 -0
  11. package/dist/runtime/components/VitaDate.vue +20 -0
  12. package/dist/runtime/components/VitaEntry.vue +24 -0
  13. package/dist/runtime/components/VitaIssues.vue +25 -0
  14. package/dist/runtime/components/VitaLinks.vue +19 -0
  15. package/dist/runtime/components/VitaMarkdown.vue +14 -0
  16. package/dist/runtime/components/VitaPublication.vue +30 -0
  17. package/dist/runtime/components/VitaPublications.vue +39 -0
  18. package/dist/runtime/components/VitaSection.vue +60 -0
  19. package/dist/runtime/composables/useVitakit.ts +47 -0
  20. package/dist/schemas/education.ts +21 -0
  21. package/dist/schemas/employment.ts +21 -0
  22. package/dist/schemas/publications.ts +48 -0
  23. package/dist/schemas/supervision.ts +20 -0
  24. package/dist/schemas/talks.ts +19 -0
  25. package/dist/schemas/teaching.ts +19 -0
  26. package/dist/shared/index.d.mts +68 -0
  27. package/dist/shared/index.mjs +57 -0
  28. package/dist/shared/vitakit.BRhjqpAt.mjs +250 -0
  29. package/dist/shared/vitakit.BlU-p01e.mjs +14 -0
  30. package/dist/shared/vitakit.BxcnlFGP.mjs +325 -0
  31. package/dist/shared/vitakit.CypCF-4Q.mjs +503 -0
  32. package/dist/shared/vitakit.D8WRg_Xx.d.mts +153 -0
  33. package/dist/styles/vitakit-math.csl +314 -0
  34. package/dist/tex/templates/_generic.eta +6 -0
  35. package/dist/tex/templates/education.eta +9 -0
  36. package/dist/tex/templates/employment.eta +9 -0
  37. package/dist/tex/templates/publications.eta +7 -0
  38. package/dist/tex/templates/supervision.eta +4 -0
  39. package/dist/tex/templates/talks.eta +3 -0
  40. package/dist/tex/templates/teaching.eta +6 -0
  41. package/dist/tex/vitakit-cv.cls +119 -0
  42. package/dist/tex.d.mts +93 -0
  43. package/dist/tex.mjs +129 -0
  44. package/package.json +121 -0
  45. package/src/bin/vitakit-build-cv.mjs +50 -0
  46. package/src/bin/vitakit-check.mjs +57 -0
  47. package/src/bin/vitakit.mjs +158 -0
  48. package/styles/vitakit-math.csl +314 -0
@@ -0,0 +1,30 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * One reference in a publication list: the reference as the citation style
4
+ * formats it, then its badges.
5
+ *
6
+ * `entry.html` is formatted at build time (title linked, formulas typeset);
7
+ * the badges are data, so a copy of this component can put them anywhere -- a
8
+ * row of buttons, icons, "arXiv:2301.00001" from `badge.value` -- without
9
+ * parsing HTML. `VitaPublications` renders one of these per entry, so ejecting
10
+ * this component alone changes every entry and keeps the grouping.
11
+ */
12
+ import type { Publication } from '@anja.codes/vitakit/shared'
13
+
14
+ defineProps<{ entry: Publication }>()
15
+ </script>
16
+
17
+ <template>
18
+ <li class="csl-entry" :data-csl-entry-id="entry.key">
19
+ <!-- eslint-disable-next-line vue/no-v-html -- build-time formatted, sanitized citation -->
20
+ <span class="vita-bib__text" v-html="entry.html" />
21
+ <!-- Spaces are explicit: the template compiler drops whitespace between lines. -->
22
+ <template v-if="entry.badges.length">
23
+ {{ ' ' }}<span class="bib-links">
24
+ <template v-for="(badge, index) in entry.badges" :key="badge.url">
25
+ {{ index ? ' ' : '' }}<a :class="badge.className" :href="badge.url">{{ badge.label }}</a>
26
+ </template>
27
+ </span>
28
+ </template>
29
+ </li>
30
+ </template>
@@ -0,0 +1,39 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Publication groups formatted from `content/publications.bib` at build time.
4
+ *
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`.
9
+ */
10
+ import { computed } from 'vue'
11
+ import { usePublications } from '#imports'
12
+
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 })
21
+
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)
26
+ </script>
27
+
28
+ <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">
32
+ {{ group.label }}
33
+ </h2>
34
+ <ol class="vita-bib csl-bib-body">
35
+ <VitaPublication v-for="entry in group.entries" :key="entry.key" :entry="entry" />
36
+ </ol>
37
+ </section>
38
+ </div>
39
+ </template>
@@ -0,0 +1,60 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * Renders any section without a dedicated component.
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.
9
+ */
10
+ import { computed } from 'vue'
11
+ import { useSection } from '#imports'
12
+ import { entrySecondaryParts, entryTitleField, richHtml, type Entry } from '@anja.codes/vitakit/shared'
13
+
14
+ const props = defineProps<{
15
+ /** Section id; ignored when `entries` is given. */
16
+ id?: string
17
+ entries?: Entry[]
18
+ /** Fields to try, in order, for the main line. */
19
+ titleFields?: string[]
20
+ limit?: number
21
+ }>()
22
+
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(' · ')
43
+
44
+ const linkOf = (entry: Entry): string | undefined =>
45
+ typeof entry.url === 'string' ? entry.url : undefined
46
+ </script>
47
+
48
+ <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>
58
+ </VitaEntry>
59
+ </ul>
60
+ </template>
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Access to the content compiled by the vitakit Nuxt module.
3
+ *
4
+ * These are plain imports of a build-time generated module: fully typed, no
5
+ * loading state, no network request, and only what a page uses ends up in its
6
+ * chunk.
7
+ */
8
+ import content from '#vitakit/content'
9
+ import type { Entry, Page, Profile, PublicationGroup, VitakitContent } from '@anja.codes/vitakit/shared'
10
+
11
+ export function useVitakit(): VitakitContent {
12
+ return content
13
+ }
14
+
15
+ export function useProfile(): Profile {
16
+ return content.profile
17
+ }
18
+
19
+ /**
20
+ * Entries of one section, already validated, visibility-filtered and sorted
21
+ * newest first.
22
+ *
23
+ * Unknown ids return an empty list rather than throwing, so deleting a content
24
+ * file cannot break a page that still mentions it.
25
+ */
26
+ export function useSection(id: string): Entry[] {
27
+ return content.sections[id] ?? []
28
+ }
29
+
30
+ /** All section ids that have content. */
31
+ export function useSectionIds(): string[] {
32
+ return Object.keys(content.sections)
33
+ }
34
+
35
+ export function usePublications(): PublicationGroup[] {
36
+ return content.publications
37
+ }
38
+
39
+ /** The page for a route, or `undefined`. Routes always end with a slash. */
40
+ export function usePage(route: string): Page | undefined {
41
+ const normalised = route.endsWith('/') ? route : `${route}/`
42
+ return content.pages.find((page: Page) => page.route === normalised)
43
+ }
44
+
45
+ export function usePages(): Page[] {
46
+ return content.pages
47
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shape of `content/education.yml`.
3
+ *
4
+ * Also the file `vitakit eject education` copies into a project, with the import
5
+ * rewritten to `@anja.codes/vitakit`; it is therefore the single definition of
6
+ * this section, not a copy of one.
7
+ */
8
+ import { dateRange, institution, link, richText, section, text, url, z } from '../primitives'
9
+
10
+ export default section({
11
+ date: dateRange,
12
+ degree: text,
13
+ institution,
14
+ thesis: z.strictObject({
15
+ title: richText,
16
+ type: text.optional(),
17
+ supervisor: z.union([text, link]).optional(),
18
+ url: url.optional(),
19
+ }).optional(),
20
+ note: text.optional(),
21
+ })
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Shape of `content/employment.yml`.
3
+ *
4
+ * Also the file `vitakit eject employment` copies into a project, with the import
5
+ * rewritten to `@anja.codes/vitakit`; it is therefore the single definition of
6
+ * this section, not a copy of one.
7
+ */
8
+ import { dateRange, institution, richText, section, text, url, z } from '../primitives'
9
+
10
+ export default section({
11
+ date: dateRange,
12
+ position: text,
13
+ institution,
14
+ project: z.strictObject({
15
+ title: richText,
16
+ funder: text.optional(),
17
+ grant: text.optional(),
18
+ url: url.optional(),
19
+ }).optional(),
20
+ note: text.optional(),
21
+ })
@@ -0,0 +1,48 @@
1
+ /**
2
+ * How `content/publications.bib` is grouped and linked -- on the website, in
3
+ * `vitakit-check` and in the PDF alike.
4
+ *
5
+ * Also the file `vitakit eject publications` copies into a project, with the
6
+ * import rewritten to `@anja.codes/vitakit`; it is therefore the single definition
7
+ * of the defaults, not a copy of them. Every setting is optional: leave out
8
+ * what you do not change.
9
+ */
10
+ import { badgePresets, publications } from '../publications'
11
+
12
+ export default publications({
13
+ /**
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.
16
+ */
17
+ groups: [
18
+ { id: 'published', label: 'Published' },
19
+ { id: 'accepted', label: 'Accepted' },
20
+ { id: 'preprint', label: 'Preprints' },
21
+ { id: 'software', label: 'Software' },
22
+ { id: 'other', label: 'Other' },
23
+ ],
24
+
25
+ /** The group of an entry without `status`; the first group if left out. */
26
+ defaultGroup: 'published',
27
+
28
+ /**
29
+ * BibTeX fields that become links after a reference on the website. The
30
+ * presets accept the spellings real exports use (`doi:10...`, doi.org URLs,
31
+ * `arXiv:...`, `MR1234567`, `Zbl 1234.56789`) and biblatex's `eprint`. A
32
+ * badge of your own, e.g. for slides in `public/slides/`:
33
+ *
34
+ * { field: 'slides', label: 'Slides', url: '/slides/$1.pdf', className: 'vita-badge' }
35
+ */
36
+ badges: [
37
+ { ...badgePresets.doi, className: 'vita-badge' },
38
+ { ...badgePresets.arxiv, className: 'vita-badge' },
39
+ { ...badgePresets.mrnumber, className: 'vita-badge' },
40
+ { ...badgePresets.zbl, className: 'vita-badge' },
41
+ ],
42
+
43
+ /**
44
+ * Further BibTeX fields a component can show, as `entry.fields.<name>`,
45
+ * e.g. `['project']` for a copy of VitaPublication that names the project.
46
+ */
47
+ fields: [],
48
+ })
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shape of `content/supervision.yml`.
3
+ *
4
+ * Also the file `vitakit eject supervision` copies into a project, with the import
5
+ * rewritten to `@anja.codes/vitakit`; it is therefore the single definition of
6
+ * this section, not a copy of one.
7
+ */
8
+ import { dateRange, institution, richText, section, text, z } from '../primitives'
9
+
10
+ export default section({
11
+ date: dateRange,
12
+ student: text,
13
+ /** A thesis still in progress; shown as such instead of as completed. */
14
+ ongoing: z.boolean().default(false),
15
+ /** Markdown, math allowed. */
16
+ title: richText,
17
+ degree: text.optional(),
18
+ institution: institution.optional(),
19
+ coSupervisor: text.optional(),
20
+ })
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shape of `content/talks.yml`.
3
+ *
4
+ * Also the file `vitakit eject talks` copies into a project, with the import
5
+ * rewritten to `@anja.codes/vitakit`; it is therefore the single definition of
6
+ * this section, not a copy of one.
7
+ */
8
+ import { dateRange, richText, section, text, url, z } from '../primitives'
9
+
10
+ export default section({
11
+ date: dateRange,
12
+ /** Markdown, math allowed. */
13
+ title: richText,
14
+ event: text.optional(),
15
+ location: text.optional(),
16
+ kind: z.enum(['invited', 'contributed', 'seminar', 'poster', 'outreach']).optional(),
17
+ url: url.optional(),
18
+ slides: url.optional(),
19
+ })
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shape of `content/teaching.yml`.
3
+ *
4
+ * Also the file `vitakit eject teaching` copies into a project, with the import
5
+ * rewritten to `@anja.codes/vitakit`; it is therefore the single definition of
6
+ * this section, not a copy of one.
7
+ */
8
+ import { dateRange, institution, richText, section, text, url } from '../primitives'
9
+
10
+ export default section({
11
+ date: dateRange,
12
+ title: richText,
13
+ role: text.optional(),
14
+ institution: institution.optional(),
15
+ /** e.g. `2024S`, `Winter 2023/24` -- free text, shown as written. */
16
+ term: text.optional(),
17
+ url: url.optional(),
18
+ note: text.optional(),
19
+ })
@@ -0,0 +1,68 @@
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';
3
+
4
+ /**
5
+ * Presentation helpers shared by the website and the LaTeX side.
6
+ *
7
+ * These live in the package because they are logic: a date rendered one way on
8
+ * the site and another way in the PDF is a bug, not a design choice.
9
+ */
10
+
11
+ interface DateFormatOptions {
12
+ locale?: string;
13
+ /** `numeric` gives 2024-03, `long` gives March 2024, `year` gives 2024. */
14
+ precision?: 'auto' | 'year';
15
+ /** Text for an open-ended range. */
16
+ presentLabel?: string;
17
+ }
18
+ /** `2019–2023`, `since 2023`, `March 2024`. */
19
+ declare function formatDateRange(range: DateRange | undefined, options?: DateFormatOptions): string;
20
+ /** A sortable key, for user code that needs to order entries itself. */
21
+ declare function dateSortKey(range: DateRange | undefined): string;
22
+
23
+ /**
24
+ * Which fields of an entry are its title and its context.
25
+ *
26
+ * A generic section has no dedicated component or template, so both sides have
27
+ * to guess: "what is the headline here, and what belongs on the second line?"
28
+ * That guess lives here, once. It used to exist twice — in the TeX emitter and
29
+ * in `VitaSection.vue` — with different field lists and different separators,
30
+ * so the website and the PDF already disagreed about the same entry.
31
+ *
32
+ * Presentation stays with each side: the website joins with `·` and renders
33
+ * HTML, the emitter joins with `,` and escapes. Only the *selection* is shared.
34
+ */
35
+
36
+ /** Fields tried, in order, for the headline. */
37
+ declare const TITLE_FIELDS: readonly string[];
38
+ /** Plain-text fields tried, in order, for the context line. */
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[];
42
+ /**
43
+ * The field holding the entry's headline, or `undefined` when nothing fits.
44
+ * Returns the field *name* so callers can render its value themselves: the
45
+ * website needs the rendered HTML, the emitter the LaTeX source.
46
+ */
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. */
49
+ type SecondaryPart = {
50
+ kind: 'text';
51
+ text: string;
52
+ } | {
53
+ kind: 'institution';
54
+ institution: Institution;
55
+ };
56
+ /**
57
+ * The context line, as structured parts. The field used for the title is
58
+ * skipped, so nothing appears twice.
59
+ */
60
+ declare function entrySecondaryParts(entry: Entry, titleField?: string): SecondaryPart[];
61
+
62
+ /** The plain-text form: Markdown source, for LaTeX, comparisons and `title=`. */
63
+ declare function richSource(value: unknown): string;
64
+ /** The rendered form, or `undefined` when there is nothing to render. */
65
+ declare function richHtml(value: unknown): string | undefined;
66
+
67
+ export { DateRange, Entry, INSTITUTION_FIELDS, Institution, SECONDARY_FIELDS, TITLE_FIELDS, dateSortKey, entrySecondaryParts, entryTitleField, formatDateRange, richHtml, richSource };
68
+ export type { DateFormatOptions, SecondaryPart };
@@ -0,0 +1,57 @@
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 };