@anja.codes/vitakit 0.1.0 → 0.2.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 (63) hide show
  1. package/README.md +51 -131
  2. package/dist/check.mjs +12 -5
  3. package/dist/chunks/authoring.mjs +367 -0
  4. package/dist/chunks/build.mjs +176 -0
  5. package/dist/chunks/documents.mjs +143 -0
  6. package/dist/chunks/draft.mjs +222 -0
  7. package/dist/chunks/eject.mjs +59 -0
  8. package/dist/chunks/emit.mjs +200 -0
  9. package/dist/chunks/eta.mjs +16 -0
  10. package/dist/chunks/fields.mjs +79 -0
  11. package/dist/chunks/inventory.mjs +199 -0
  12. package/dist/chunks/plan.mjs +144 -0
  13. package/dist/chunks/prompt.mjs +33 -0
  14. package/dist/chunks/schema-source.mjs +12 -0
  15. package/dist/chunks/schemas.mjs +109 -0
  16. package/dist/chunks/sections.mjs +109 -0
  17. package/dist/chunks/snippets.mjs +199 -0
  18. package/dist/chunks/validate.mjs +124 -0
  19. package/dist/chunks/vue.mjs +116 -0
  20. package/dist/cli.d.mts +33 -40
  21. package/dist/cli.mjs +7 -224
  22. package/dist/index.d.mts +372 -42
  23. package/dist/index.mjs +10 -4
  24. package/dist/nuxt.mjs +35 -5
  25. package/dist/runtime/components/VitaSection.vue +94 -38
  26. package/dist/schemas/education.ts +4 -4
  27. package/dist/schemas/employment.ts +3 -3
  28. package/dist/schemas/publications.ts +2 -2
  29. package/dist/schemas/supervision.ts +3 -3
  30. package/dist/schemas/talks.ts +1 -1
  31. package/dist/schemas/teaching.ts +3 -3
  32. package/dist/shared/index.d.mts +60 -9
  33. package/dist/shared/index.mjs +3 -57
  34. package/dist/shared/vitakit.BZZIsWlY.mjs +179 -0
  35. package/dist/shared/vitakit.BerUaxtF.mjs +121 -0
  36. package/dist/shared/{vitakit.D8WRg_Xx.d.mts → vitakit.BfpxDtz1.d.mts} +9 -8
  37. package/dist/shared/{vitakit.CypCF-4Q.mjs → vitakit.By5VWQ1n.mjs} +12 -78
  38. package/dist/shared/vitakit.CfB3hMHl.mjs +60 -0
  39. package/dist/shared/vitakit.CoWX2Z4R.mjs +1554 -0
  40. package/dist/shared/{vitakit.BlU-p01e.mjs → vitakit.CtChKO0I.mjs} +1 -1
  41. package/dist/shared/vitakit.D-DdJaDZ.mjs +20 -0
  42. package/dist/shared/vitakit.D10HyDhF.mjs +19 -0
  43. package/dist/shared/vitakit.DQ6PmGoL.mjs +34 -0
  44. package/dist/shared/vitakit.DbSeENVS.mjs +58 -0
  45. package/dist/shared/vitakit.DcQOdM4I.mjs +57 -0
  46. package/dist/shared/vitakit.Do5HC_MD.mjs +142 -0
  47. package/dist/shared/vitakit.TSMTC5Jl.mjs +6 -0
  48. package/dist/tex/templates/_generic.eta +6 -4
  49. package/dist/tex/templates/education.eta +10 -5
  50. package/dist/tex/templates/employment.eta +12 -5
  51. package/dist/tex/templates/publications.eta +4 -2
  52. package/dist/tex/templates/supervision.eta +10 -2
  53. package/dist/tex/templates/talks.eta +5 -1
  54. package/dist/tex/templates/teaching.eta +6 -4
  55. package/dist/tex/vitakit-cv.cls +21 -6
  56. package/dist/tex.d.mts +53 -5
  57. package/dist/tex.mjs +20 -113
  58. package/package.json +10 -7
  59. package/src/bin/vitakit.mjs +3 -153
  60. package/dist/shared/vitakit.BRhjqpAt.mjs +0 -250
  61. package/dist/shared/vitakit.BxcnlFGP.mjs +0 -325
  62. package/src/bin/vitakit-build-cv.mjs +0 -50
  63. package/src/bin/vitakit-check.mjs +0 -57
@@ -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>
@@ -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,8 @@
1
1
  /**
2
2
  * How `content/publications.bib` is grouped and linked -- on the website, in
3
- * `vitakit-check` and in the PDF alike.
3
+ * `vitakit verify` and in the PDF alike.
4
4
  *
5
- * Also the file `vitakit eject publications` copies into a project, with the
5
+ * Also the file `vitakit customize publications` writes into a project, with the
6
6
  * import rewritten to `@anja.codes/vitakit`; it is therefore the single definition
7
7
  * of the defaults, not a copy of them. Every setting is optional: leave out
8
8
  * what you do not change.
@@ -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.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';
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';
@@ -0,0 +1,179 @@
1
+ import { parse } from '@babel/parser';
2
+
3
+ const PACKAGE = "@anja.codes/vitakit";
4
+ function parseModule(source) {
5
+ return parse(source, { sourceType: "module", plugins: ["typescript"] }).program;
6
+ }
7
+ function unwrap(expression) {
8
+ let current = expression;
9
+ while (current.type === "TSAsExpression" || current.type === "TSSatisfiesExpression" || current.type === "ParenthesizedExpression" || current.type === "TSNonNullExpression") {
10
+ current = current.expression;
11
+ }
12
+ return current;
13
+ }
14
+ function packageImports(body) {
15
+ const names = /* @__PURE__ */ new Map();
16
+ for (const statement of body) {
17
+ if (statement.type !== "ImportDeclaration" || statement.source.value !== PACKAGE) continue;
18
+ for (const specifier of statement.specifiers) {
19
+ if (specifier.type !== "ImportSpecifier") continue;
20
+ const imported = specifier.imported.type === "Identifier" ? specifier.imported.name : specifier.imported.value;
21
+ names.set(specifier.local.name, imported);
22
+ }
23
+ }
24
+ return names;
25
+ }
26
+ function defaultExport(body) {
27
+ const exported = body.find((statement) => statement.type === "ExportDefaultDeclaration");
28
+ if (!exported || exported.type !== "ExportDefaultDeclaration") return void 0;
29
+ const declaration = exported.declaration;
30
+ if (declaration.type === "Identifier") {
31
+ for (const statement of body) {
32
+ if (statement.type !== "VariableDeclaration" || statement.kind !== "const") continue;
33
+ for (const declarator of statement.declarations) {
34
+ if (declarator.id.type === "Identifier" && declarator.id.name === declaration.name && declarator.init) {
35
+ return unwrap(declarator.init);
36
+ }
37
+ }
38
+ }
39
+ return void 0;
40
+ }
41
+ if (declaration.type.endsWith("Declaration")) return void 0;
42
+ return unwrap(declaration);
43
+ }
44
+ function keyName(property) {
45
+ if (property.type !== "ObjectProperty" || property.computed) return void 0;
46
+ if (property.key.type === "Identifier") return property.key.name;
47
+ if (property.key.type === "StringLiteral") return property.key.value;
48
+ return void 0;
49
+ }
50
+ function recognizeSchema(source) {
51
+ let body;
52
+ try {
53
+ body = parseModule(source).body;
54
+ } catch (error) {
55
+ return { ok: false, reason: `cannot be parsed: ${error instanceof Error ? error.message : String(error)}` };
56
+ }
57
+ const expression = defaultExport(body);
58
+ if (!expression || expression.type !== "CallExpression" || expression.callee.type !== "Identifier") {
59
+ return { ok: false, reason: "its default export is not a section(...) or extendSection(...) call" };
60
+ }
61
+ const call = expression;
62
+ const kind = packageImports(body).get(call.callee.name);
63
+ const object = kind === "section" ? call.arguments[0] : kind === "extendSection" ? call.arguments[1] : void 0;
64
+ const expected = kind === "section" ? 1 : 2;
65
+ if (kind !== "section" && kind !== "extendSection" || call.arguments.length !== expected || !object || object.type !== "ObjectExpression") {
66
+ return { ok: false, reason: `its default export is not section({ ... }) or extendSection(base, { ... }) from ${PACKAGE}` };
67
+ }
68
+ const fields = [];
69
+ for (const property of object.properties) {
70
+ const name = keyName(property);
71
+ if (name === void 0) return { ok: false, reason: "its fields contain a spread or a computed key" };
72
+ fields.push(name);
73
+ }
74
+ return {
75
+ ok: true,
76
+ value: {
77
+ kind,
78
+ fields,
79
+ start: object.start,
80
+ end: object.end,
81
+ properties: object.properties.map((property) => ({ start: property.start, end: property.end }))
82
+ }
83
+ };
84
+ }
85
+ const IDENTIFIER = /^[A-Za-z_$][\w$]*$/;
86
+ function propertyKey(name) {
87
+ return IDENTIFIER.test(name) ? name : `'${name.replace(/\\/g, "\\\\").replace(/'/g, "\\'")}'`;
88
+ }
89
+ function lineStart(source, offset) {
90
+ return source.lastIndexOf("\n", offset - 1) + 1;
91
+ }
92
+ function indentationAt(source, offset) {
93
+ return /^[ \t]*/.exec(source.slice(lineStart(source, offset)))[0];
94
+ }
95
+ function endOfLineContent(source, offset) {
96
+ let position = offset;
97
+ for (; ; ) {
98
+ while (source[position] === " " || source[position] === " ") position++;
99
+ if (source.startsWith("//", position)) {
100
+ const end = source.indexOf("\n", position);
101
+ return end === -1 ? source.length : end;
102
+ }
103
+ if (source.startsWith("/*", position)) {
104
+ const end = source.indexOf("*/", position + 2);
105
+ if (end === -1 || source.slice(position, end).includes("\n")) return position;
106
+ position = end + 2;
107
+ continue;
108
+ }
109
+ return position;
110
+ }
111
+ }
112
+ function addSchemaField(source, name, code) {
113
+ const recognized = recognizeSchema(source);
114
+ if (!recognized.ok) return recognized;
115
+ const { fields, properties, start: open, end } = recognized.value;
116
+ if (fields.includes(name)) return { ok: false, reason: `it already has a field "${name}"` };
117
+ const entry = `${propertyKey(name)}: ${code}`;
118
+ const close = end - 1;
119
+ if (!properties.length) {
120
+ const inner = source.slice(open + 1, close);
121
+ if (!inner.includes("\n")) {
122
+ const indent3 = indentationAt(source, open);
123
+ return { ok: true, value: `${source.slice(0, open)}{
124
+ ${indent3} ${entry},
125
+ ${indent3}}${source.slice(close + 1)}` };
126
+ }
127
+ const closeLine = lineStart(source, close);
128
+ const indent2 = `${indentationAt(source, close)} `;
129
+ if (source.slice(closeLine, close).trim() !== "") {
130
+ return { ok: true, value: `${source.slice(0, close)}
131
+ ${indent2}${entry},
132
+ ${source.slice(close)}` };
133
+ }
134
+ return { ok: true, value: `${source.slice(0, closeLine)}${indent2}${entry},
135
+ ${source.slice(closeLine)}` };
136
+ }
137
+ const first = properties[0];
138
+ const last = properties[properties.length - 1];
139
+ const multiline = source.slice(open, close).includes("\n") && source.slice(lineStart(source, first.start), first.start).trim() === "";
140
+ let afterLast = last.end;
141
+ while (source[afterLast] === " " || source[afterLast] === " ") afterLast++;
142
+ const comma = source[afterLast] === ",";
143
+ if (!multiline) {
144
+ return { ok: true, value: `${source.slice(0, last.end)}, ${entry}${source.slice(last.end)}` };
145
+ }
146
+ const indent = indentationAt(source, first.start);
147
+ const insertAt = endOfLineContent(source, comma ? afterLast + 1 : last.end);
148
+ const head = comma ? source.slice(0, insertAt) : `${source.slice(0, last.end)},${source.slice(last.end, insertAt)}`;
149
+ return { ok: true, value: `${head}
150
+ ${indent}${entry}${comma ? "," : ""}${source.slice(insertAt)}` };
151
+ }
152
+ function ensurePackageImports(source, names) {
153
+ let body;
154
+ try {
155
+ body = parseModule(source).body;
156
+ } catch (error) {
157
+ return { ok: false, reason: `cannot be parsed: ${error instanceof Error ? error.message : String(error)}` };
158
+ }
159
+ const imported = packageImports(body);
160
+ const local = new Set(imported.keys());
161
+ const missing = names.filter((name) => !local.has(name));
162
+ if (!missing.length) return { ok: true, value: source };
163
+ const declaration = body.find((statement) => statement.type === "ImportDeclaration" && statement.source.value === PACKAGE && statement.importKind !== "type" && statement.specifiers.some((specifier) => specifier.type === "ImportSpecifier"));
164
+ if (declaration) {
165
+ const specifiers = declaration.specifiers.filter((specifier) => specifier.type === "ImportSpecifier");
166
+ const at2 = specifiers[specifiers.length - 1].end;
167
+ return { ok: true, value: `${source.slice(0, at2)}, ${missing.join(", ")}${source.slice(at2)}` };
168
+ }
169
+ const imports = body.filter((statement) => statement.type === "ImportDeclaration");
170
+ const line = `import { ${missing.join(", ")} } from '${PACKAGE}'
171
+ `;
172
+ if (!imports.length) return { ok: true, value: line + source };
173
+ const at = imports[imports.length - 1].end;
174
+ const end = source.indexOf("\n", at);
175
+ const position = end === -1 ? source.length : end + 1;
176
+ return { ok: true, value: `${source.slice(0, position)}${end === -1 ? "\n" : ""}${line}${source.slice(position)}` };
177
+ }
178
+
179
+ export { addSchemaField as a, ensurePackageImports as e, propertyKey as p };