@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.
- package/README.md +4 -3
- package/dist/check.mjs +14 -12
- package/dist/chunks/authoring.mjs +50 -45
- package/dist/{shared/vitakit.By5VWQ1n.mjs → chunks/bibliography.mjs} +73 -170
- package/dist/chunks/build.mjs +19 -17
- package/dist/chunks/documents.mjs +15 -6
- package/dist/chunks/draft.mjs +3 -3
- package/dist/chunks/eject.mjs +2 -2
- package/dist/chunks/emit.mjs +38 -22
- package/dist/chunks/fields.mjs +2 -2
- package/dist/chunks/inventory.mjs +29 -27
- package/dist/chunks/migrations.mjs +218 -0
- package/dist/chunks/plan.mjs +36 -8
- package/dist/chunks/publication-lists.mjs +202 -0
- package/dist/chunks/schemas.mjs +2 -2
- package/dist/chunks/sections.mjs +3 -3
- package/dist/chunks/validate.mjs +25 -16
- package/dist/chunks/vue.mjs +89 -52
- package/dist/chunks/yaml.mjs +62 -0
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +9 -9
- package/dist/index.mjs +12 -10
- package/dist/nuxt.mjs +19 -11
- package/dist/runtime/components/VitaPublications.vue +21 -22
- package/dist/runtime/composables/useVitakit.ts +14 -3
- package/dist/schemas/publications.ts +9 -7
- package/dist/shared/index.d.mts +2 -2
- package/dist/shared/{vitakit.BfpxDtz1.d.mts → vitakit.BfR_mUHm.d.mts} +17 -5
- package/dist/shared/{vitakit.BZZIsWlY.mjs → vitakit.CCTDg7pz.mjs} +21 -1
- package/dist/shared/{vitakit.DbSeENVS.mjs → vitakit.DCK90zq4.mjs} +10 -1
- package/dist/shared/vitakit.DteA9vLy.mjs +135 -0
- package/dist/shared/{vitakit.BerUaxtF.mjs → vitakit.DvABWuUc.mjs} +1 -1
- package/dist/shared/{vitakit.Do5HC_MD.mjs → vitakit.DvvUYUTS.mjs} +61 -3
- package/dist/shared/vitakit.UfoXdL7U.mjs +22 -0
- package/dist/shared/{vitakit.CoWX2Z4R.mjs → vitakit.zwr1ssO5.mjs} +353 -57
- package/dist/tex/templates/publications.eta +4 -2
- package/dist/tex/vitakit-cv.cls +15 -4
- package/dist/tex.d.mts +29 -8
- package/dist/tex.mjs +13 -10
- 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.
|
|
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.
|
|
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
|
-
*
|
|
3
|
+
* A publication list from `content/publication-lists.yml`, formatted from
|
|
4
|
+
* `content/publications.bib` at build time.
|
|
4
5
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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 {
|
|
12
|
+
import { usePublicationList } from '#imports'
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
<
|
|
31
|
-
|
|
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
|
-
</
|
|
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,
|
|
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
|
-
|
|
36
|
-
|
|
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
|
-
*
|
|
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,
|
|
15
|
-
*
|
|
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
|
-
|
|
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
|
|
26
|
-
|
|
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
|
package/dist/shared/index.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { D as DateRange, d as Linked, L as Link, E as Entry } from './vitakit.
|
|
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.
|
|
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
|
|
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
|
-
|
|
98
|
+
/** The group's heading; none for the single group of an ungrouped list. */
|
|
99
|
+
label?: string;
|
|
98
100
|
count: number;
|
|
99
101
|
/**
|
|
100
|
-
*
|
|
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
|
|
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,
|
|
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
|
-
|
|
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) => ({ "&": "&", "<": "<", ">": ">" })[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 };
|
|
@@ -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 === "
|
|
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 {
|
|
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 };
|