@singi-labs/sifa-sdk 0.19.90 → 0.19.92
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/dist/index.cjs +1 -1
- package/dist/index.js +1 -1
- package/dist/resume/export-contact.js +23 -5
- package/dist/resume/export-contact.js.map +1 -1
- package/dist/resume/funder-cv.js.map +1 -1
- package/dist/resume/index.cjs +23 -5
- package/dist/resume/index.cjs.map +1 -1
- package/dist/resume/index.d.cts +30 -6
- package/dist/resume/index.d.ts +30 -6
- package/dist/resume/json-resume.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -7692,7 +7692,7 @@ function describeSifaRecord(collection, value) {
|
|
|
7692
7692
|
}
|
|
7693
7693
|
|
|
7694
7694
|
// src/index.ts
|
|
7695
|
-
var SIFA_SDK_VERSION = "0.19.
|
|
7695
|
+
var SIFA_SDK_VERSION = "0.19.92";
|
|
7696
7696
|
|
|
7697
7697
|
exports.ACTIVITY_TIERS = ACTIVITY_TIERS;
|
|
7698
7698
|
exports.ACTIVITY_VERBS = ACTIVITY_VERBS;
|
package/dist/index.js
CHANGED
|
@@ -377,7 +377,7 @@ import {
|
|
|
377
377
|
roadmapIssueUrl
|
|
378
378
|
} from "./roadmap/index.js";
|
|
379
379
|
import { SIFA_REPO_GROUPS, describeSifaRecord, repoGroupForCollection } from "./repo/index.js";
|
|
380
|
-
var SIFA_SDK_VERSION = "0.19.
|
|
380
|
+
var SIFA_SDK_VERSION = "0.19.92";
|
|
381
381
|
export {
|
|
382
382
|
ACTIVITY_TIERS,
|
|
383
383
|
ACTIVITY_VERBS,
|
|
@@ -3,28 +3,43 @@ import { z } from "zod";
|
|
|
3
3
|
var EXPORT_CONTACT_LIMITS = {
|
|
4
4
|
email: 254,
|
|
5
5
|
phone: 40,
|
|
6
|
-
address: 500
|
|
6
|
+
address: 500,
|
|
7
|
+
driversLicense: 60,
|
|
8
|
+
rightToWork: 200,
|
|
9
|
+
nationality: 100
|
|
7
10
|
};
|
|
8
11
|
var exportContactSchema = z.strictObject({
|
|
9
12
|
email: z.string().max(EXPORT_CONTACT_LIMITS.email).optional(),
|
|
10
13
|
phone: z.string().max(EXPORT_CONTACT_LIMITS.phone).optional(),
|
|
11
|
-
address: z.string().max(EXPORT_CONTACT_LIMITS.address).optional()
|
|
14
|
+
address: z.string().max(EXPORT_CONTACT_LIMITS.address).optional(),
|
|
15
|
+
driversLicense: z.string().max(EXPORT_CONTACT_LIMITS.driversLicense).optional(),
|
|
16
|
+
rightToWork: z.string().max(EXPORT_CONTACT_LIMITS.rightToWork).optional(),
|
|
17
|
+
nationality: z.string().max(EXPORT_CONTACT_LIMITS.nationality).optional()
|
|
12
18
|
});
|
|
13
19
|
function pickExportContact(draft) {
|
|
14
20
|
return normalizeExportContact({
|
|
15
21
|
email: draft.includeEmail ? draft.email : void 0,
|
|
16
22
|
phone: draft.includePhone ? draft.phone : void 0,
|
|
17
|
-
address: draft.includeAddress ? draft.address : void 0
|
|
23
|
+
address: draft.includeAddress ? draft.address : void 0,
|
|
24
|
+
driversLicense: draft.includeDriversLicense ? draft.driversLicense : void 0,
|
|
25
|
+
rightToWork: draft.includeRightToWork ? draft.rightToWork : void 0,
|
|
26
|
+
nationality: draft.includeNationality ? draft.nationality : void 0
|
|
18
27
|
});
|
|
19
28
|
}
|
|
20
29
|
function normalizeExportContact(contact) {
|
|
21
30
|
const email = cap(oneLine(contact.email), EXPORT_CONTACT_LIMITS.email);
|
|
22
31
|
const phone = cap(oneLine(contact.phone), EXPORT_CONTACT_LIMITS.phone);
|
|
23
32
|
const address = cap(lines(contact.address), EXPORT_CONTACT_LIMITS.address);
|
|
33
|
+
const driversLicense = cap(oneLine(contact.driversLicense), EXPORT_CONTACT_LIMITS.driversLicense);
|
|
34
|
+
const rightToWork = cap(oneLine(contact.rightToWork), EXPORT_CONTACT_LIMITS.rightToWork);
|
|
35
|
+
const nationality = cap(oneLine(contact.nationality), EXPORT_CONTACT_LIMITS.nationality);
|
|
24
36
|
return {
|
|
25
37
|
...email ? { email } : {},
|
|
26
38
|
...phone ? { phone } : {},
|
|
27
|
-
...address ? { address } : {}
|
|
39
|
+
...address ? { address } : {},
|
|
40
|
+
...driversLicense ? { driversLicense } : {},
|
|
41
|
+
...rightToWork ? { rightToWork } : {},
|
|
42
|
+
...nationality ? { nationality } : {}
|
|
28
43
|
};
|
|
29
44
|
}
|
|
30
45
|
function looksLikeEmail(value) {
|
|
@@ -37,7 +52,10 @@ function looksLikeEmail(value) {
|
|
|
37
52
|
return local.length > 0 && dot > 0 && dot < domain.length - 1;
|
|
38
53
|
}
|
|
39
54
|
function cap(value, max) {
|
|
40
|
-
|
|
55
|
+
if (value === void 0) return void 0;
|
|
56
|
+
let cut = value.slice(0, max);
|
|
57
|
+
if (/[\uD800-\uDBFF]$/.test(cut)) cut = cut.slice(0, -1);
|
|
58
|
+
return cut.trimEnd();
|
|
41
59
|
}
|
|
42
60
|
function oneLine(value) {
|
|
43
61
|
const collapsed = value?.replace(/\s+/g, " ").trim();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/resume/export-contact.ts"],"sourcesContent":["/**\n * Private contact details for exports: email, phone
|
|
1
|
+
{"version":3,"sources":["../../src/resume/export-contact.ts"],"sourcesContent":["/**\n * Private contact and personal details for exports: email, phone, postal\n * address, driver's license, right to work and nationality.\n *\n * Sifa never stores these and never writes them to the owner's PDS, where they\n * would be public. The owner types them on the export page, the browser keeps\n * them, and they go only into the files the owner downloads. This module holds\n * the shape, the limits and the pick rule, so the export page and the server\n * that builds a funder CV agree on them.\n */\n\nimport { z } from 'zod';\n\n/** Longest value accepted per field. 254 is the longest valid email address. */\nexport const EXPORT_CONTACT_LIMITS = {\n email: 254,\n phone: 40,\n address: 500,\n driversLicense: 60,\n rightToWork: 200,\n nationality: 100,\n} as const;\n\n/** The contact details that go into an export. Absent keys are left out. */\nexport interface ExportContact {\n readonly email?: string;\n readonly phone?: string;\n /** Free text; lines are separated by `\\n`. */\n readonly address?: string;\n /** License categories, e.g. \"B, BE\". */\n readonly driversLicense?: string;\n /** e.g. \"EU citizen, no sponsorship needed\". */\n readonly rightToWork?: string;\n readonly nationality?: string;\n}\n\n/** What the owner has typed, plus which fields they chose to include. */\nexport interface ExportContactDraft {\n readonly email?: string;\n readonly phone?: string;\n readonly address?: string;\n readonly driversLicense?: string;\n readonly rightToWork?: string;\n readonly nationality?: string;\n readonly includeEmail?: boolean;\n readonly includePhone?: boolean;\n readonly includeAddress?: boolean;\n readonly includeDriversLicense?: boolean;\n readonly includeRightToWork?: boolean;\n readonly includeNationality?: boolean;\n}\n\n/**\n * Validates a picked contact. Only lengths are checked: phone numbers and\n * addresses vary too much between countries, and a wrong-looking email is a\n * warning for the owner, not a reason to refuse the export.\n */\nexport const exportContactSchema = z.strictObject({\n email: z.string().max(EXPORT_CONTACT_LIMITS.email).optional(),\n phone: z.string().max(EXPORT_CONTACT_LIMITS.phone).optional(),\n address: z.string().max(EXPORT_CONTACT_LIMITS.address).optional(),\n driversLicense: z.string().max(EXPORT_CONTACT_LIMITS.driversLicense).optional(),\n rightToWork: z.string().max(EXPORT_CONTACT_LIMITS.rightToWork).optional(),\n nationality: z.string().max(EXPORT_CONTACT_LIMITS.nationality).optional(),\n});\n\n/**\n * The fields to put in an export: the included ones that are not blank, run\n * through {@link normalizeExportContact}.\n */\nexport function pickExportContact(draft: ExportContactDraft): ExportContact {\n return normalizeExportContact({\n email: draft.includeEmail ? draft.email : undefined,\n phone: draft.includePhone ? draft.phone : undefined,\n address: draft.includeAddress ? draft.address : undefined,\n driversLicense: draft.includeDriversLicense ? draft.driversLicense : undefined,\n rightToWork: draft.includeRightToWork ? draft.rightToWork : undefined,\n nationality: draft.includeNationality ? draft.nationality : undefined,\n });\n}\n\n/**\n * Drops blank fields and tidies the rest. Every field but the address becomes\n * one line with single spaces. An address keeps its line breaks but loses blank lines and the\n * spaces around each line. Each field is cut at its {@link EXPORT_CONTACT_LIMITS}\n * length. The emitters call this too, so a caller that skips\n * {@link pickExportContact} and the schema gets the same bounded output.\n */\nexport function normalizeExportContact(contact: ExportContact): ExportContact {\n const email = cap(oneLine(contact.email), EXPORT_CONTACT_LIMITS.email);\n const phone = cap(oneLine(contact.phone), EXPORT_CONTACT_LIMITS.phone);\n const address = cap(lines(contact.address), EXPORT_CONTACT_LIMITS.address);\n const driversLicense = cap(oneLine(contact.driversLicense), EXPORT_CONTACT_LIMITS.driversLicense);\n const rightToWork = cap(oneLine(contact.rightToWork), EXPORT_CONTACT_LIMITS.rightToWork);\n const nationality = cap(oneLine(contact.nationality), EXPORT_CONTACT_LIMITS.nationality);\n return {\n ...(email ? { email } : {}),\n ...(phone ? { phone } : {}),\n ...(address ? { address } : {}),\n ...(driversLicense ? { driversLicense } : {}),\n ...(rightToWork ? { rightToWork } : {}),\n ...(nationality ? { nationality } : {}),\n };\n}\n\n/** A loose shape check for a non-blocking \"check this address\" hint. */\nexport function looksLikeEmail(value: string): boolean {\n const trimmed = value.trim();\n if (/\\s/.test(trimmed)) return false;\n const parts = trimmed.split('@');\n if (parts.length !== 2) return false;\n const [local, domain] = parts as [string, string];\n const dot = domain.lastIndexOf('.');\n return local.length > 0 && dot > 0 && dot < domain.length - 1;\n}\n\nfunction cap(value: string | undefined, max: number): string | undefined {\n if (value === undefined) return undefined;\n let cut = value.slice(0, max);\n // Never leave half of a surrogate pair (an emoji, say) at the cut.\n if (/[\\uD800-\\uDBFF]$/.test(cut)) cut = cut.slice(0, -1);\n return cut.trimEnd();\n}\n\nfunction oneLine(value: string | undefined): string | undefined {\n const collapsed = value?.replace(/\\s+/g, ' ').trim();\n return collapsed ? collapsed : undefined;\n}\n\nfunction lines(value: string | undefined): string | undefined {\n const kept = (value ?? '')\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean);\n return kept.length > 0 ? kept.join('\\n') : undefined;\n}\n"],"mappings":"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","names":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/resume/funder-cv.ts"],"sourcesContent":["/**\n * Funder-format CV documents: the NIH Biographical Sketch (Common Form plus\n * NIH Supplement) and the ERC Part B1 \"Curriculum vitae and Track Record\".\n *\n * A pure mapper from a Sifa profile and the owner's pick of items to a plain\n * JSON document a typesetter lays out. Like the JSON Resume emitter next door,\n * nothing here fetches or renders.\n *\n * Funders cap how many items a CV may list. The caps live in\n * `FUNDER_CV_GROUPS`, so the export page that offers the choice and the server\n * that enforces it read one number. Sections a funder asks for that Sifa holds\n * no records for (a personal statement, contributions to science, career\n * breaks) are emitted as `null` or empty, for the template to show as a\n * placeholder the applicant fills in.\n */\n\nimport { z } from 'zod';\n\nimport { formatLocation } from '../format/location-utils.js';\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport { isTeachingCourse } from '../taxonomy/course-role.js';\nimport type {\n ExternalAccount,\n ProfileCourse,\n ProfileEducation,\n ProfileHonor,\n ProfilePosition,\n ProfilePresentation,\n ProfilePublication,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normalizeExportContact, type ExportContact } from './export-contact.js';\nimport { toResumeDate } from './json-resume.js';\n\nexport const FUNDER_CV_FORMATS = ['nih', 'erc'] as const;\nexport type FunderCvFormat = (typeof FUNDER_CV_FORMATS)[number];\n\nexport const FUNDER_CV_ITEM_KINDS = ['publication', 'presentation', 'honor'] as const;\nexport type FunderCvItemKind = (typeof FUNDER_CV_ITEM_KINDS)[number];\n\nexport interface FunderCvGroupSpec {\n readonly kinds: readonly FunderCvItemKind[];\n /** Most items the funder's form takes for this group. */\n readonly max: number;\n}\n\n/**\n * The pickable groups per format, with the funder's item limits.\n *\n * - NIH Common Form \"Products\": up to 5 closely related to the proposed\n * project and up to 5 other significant products. Publications, conference\n * papers and presentations all count. NIH Supplement \"Honors\": up to 15.\n * - ERC Part B1 \"Research achievements\": up to ten research outputs. \"Peer\n * recognition\" is \"selected examples\" with no number; 15 is Sifa's own cap,\n * to keep the section inside the 4-page limit.\n */\nexport const FUNDER_CV_GROUPS = {\n nih: {\n related: { kinds: ['publication', 'presentation'], max: 5 },\n other: { kinds: ['publication', 'presentation'], max: 5 },\n honors: { kinds: ['honor'], max: 15 },\n },\n erc: {\n outputs: { kinds: ['publication'], max: 10 },\n recognition: { kinds: ['honor', 'presentation'], max: 15 },\n },\n} as const satisfies Record<FunderCvFormat, Record<string, FunderCvGroupSpec>>;\n\nexport type FunderCvGroupName<F extends FunderCvFormat> = keyof (typeof FUNDER_CV_GROUPS)[F] &\n string;\n\n/** A picked item: `<kind>:<rkey>`, e.g. `publication:3kxyz`. */\nexport function funderCvItemId(kind: FunderCvItemKind, rkey: string): string {\n return `${kind}:${rkey}`;\n}\n\n/** Record key syntax (atproto): 1-512 of A-Z a-z 0-9 . - _ : ~ */\nconst ITEM_ID_RE = /^(publication|presentation|honor):[A-Za-z0-9._~:-]{1,512}$/;\n\nfunction kindOf(id: string): FunderCvItemKind | undefined {\n const kind = id.slice(0, id.indexOf(':'));\n return (FUNDER_CV_ITEM_KINDS as readonly string[]).includes(kind)\n ? (kind as FunderCvItemKind)\n : undefined;\n}\n\nfunction groupSchema(spec: FunderCvGroupSpec) {\n return z\n .array(\n z\n .string()\n .regex(ITEM_ID_RE)\n .refine((id) => spec.kinds.includes(kindOf(id) as FunderCvItemKind), {\n message: `Only ${spec.kinds.join(', ')} items fit here`,\n }),\n )\n .max(spec.max)\n .default([]);\n}\n\nfunction noRepeats(lists: readonly string[][]): boolean {\n const all = lists.flat();\n return new Set(all).size === all.length;\n}\n\nconst nihSelectionSchema = z\n .object({\n format: z.literal('nih'),\n related: groupSchema(FUNDER_CV_GROUPS.nih.related),\n other: groupSchema(FUNDER_CV_GROUPS.nih.other),\n honors: groupSchema(FUNDER_CV_GROUPS.nih.honors),\n })\n .refine((s) => noRepeats([s.related, s.other, s.honors]), {\n message: 'An item can be listed once',\n });\n\nconst ercSelectionSchema = z\n .object({\n format: z.literal('erc'),\n outputs: groupSchema(FUNDER_CV_GROUPS.erc.outputs),\n recognition: groupSchema(FUNDER_CV_GROUPS.erc.recognition),\n })\n .refine((s) => noRepeats([s.outputs, s.recognition]), {\n message: 'An item can be listed once',\n });\n\n/**\n * The owner's pick for one format. Enforces the shape and the funder's limits;\n * whether each id is one of the owner's own visible records is a separate\n * check against the profile, `findUnknownFunderCvItems`.\n */\nexport const funderCvSelectionSchema = z.discriminatedUnion('format', [\n nihSelectionSchema,\n ercSelectionSchema,\n]);\n\nexport type FunderCvSelection = z.infer<typeof funderCvSelectionSchema>;\n\n/** The subset of `Profile` the funder mapper reads. */\nexport interface FunderCvProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly headline?: string;\n readonly website?: string;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly publications?: ProfilePublication[];\n readonly presentations?: ProfilePresentation[];\n readonly honors?: ProfileHonor[];\n readonly courses?: ProfileCourse[];\n readonly volunteering?: ProfileVolunteering[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface FunderCvCandidate {\n readonly id: string;\n readonly kind: FunderCvItemKind;\n readonly title: string;\n /** `YYYY`, `YYYY-MM` or `YYYY-MM-DD`; absent when the record has no date. */\n readonly date?: string;\n}\n\n/** The most recent visible delivery of a talk, which dates it. */\nfunction latestDeliveryRecord(presentation: ProfilePresentation) {\n const dateOf = (d: { date?: string | null }) => toResumeDate(d.date ?? undefined) ?? '';\n return [...filterHidden(presentation.deliveries)]\n .sort((a, b) => dateOf(a).localeCompare(dateOf(b)))\n .at(-1);\n}\n\nfunction latestDelivery(presentation: ProfilePresentation): string | undefined {\n return toResumeDate(latestDeliveryRecord(presentation)?.date ?? undefined);\n}\n\nfunction candidatesOf(profile: FunderCvProfileInput, kind: FunderCvItemKind): FunderCvCandidate[] {\n switch (kind) {\n case 'publication':\n return filterHidden(profile.publications).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: toResumeDate(p.date),\n }));\n case 'presentation':\n return filterHidden(profile.presentations).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: latestDelivery(p),\n }));\n case 'honor':\n return filterHidden(profile.honors).map((h) => ({\n id: funderCvItemId(kind, h.rkey),\n kind,\n title: h.title,\n date: toResumeDate(h.date),\n }));\n }\n}\n\n/**\n * The owner's visible items of the given kinds, most recent first. Undated\n * items go last, in profile order. The sort is stable, so equal dates keep\n * profile order too.\n */\nexport function listFunderCvCandidates(\n profile: FunderCvProfileInput,\n kinds: readonly FunderCvItemKind[],\n): FunderCvCandidate[] {\n const all = kinds.flatMap((kind) => candidatesOf(profile, kind));\n return all.sort((a, b) => {\n if (a.date && b.date) return b.date.localeCompare(a.date);\n if (a.date) return -1;\n if (b.date) return 1;\n return 0;\n });\n}\n\n/**\n * What the export page pre-ticks: the most recent publications up to each\n * limit (for NIH, the first five as closely related and the next five as\n * other), and the most recent honors. Talks are offered but not pre-ticked.\n */\nexport function defaultFunderCvSelection(\n profile: FunderCvProfileInput,\n format: FunderCvFormat,\n): FunderCvSelection {\n const publications = listFunderCvCandidates(profile, ['publication']).map((c) => c.id);\n const honors = listFunderCvCandidates(profile, ['honor']).map((c) => c.id);\n if (format === 'nih') {\n const { related, other } = FUNDER_CV_GROUPS.nih;\n return {\n format,\n related: publications.slice(0, related.max),\n other: publications.slice(related.max, related.max + other.max),\n honors: honors.slice(0, FUNDER_CV_GROUPS.nih.honors.max),\n };\n }\n return {\n format,\n outputs: publications.slice(0, FUNDER_CV_GROUPS.erc.outputs.max),\n recognition: honors.slice(0, FUNDER_CV_GROUPS.erc.recognition.max),\n };\n}\n\nfunction selectedIds(selection: FunderCvSelection): string[] {\n return selection.format === 'nih'\n ? [...selection.related, ...selection.other, ...selection.honors]\n : [...selection.outputs, ...selection.recognition];\n}\n\n/**\n * Ids in the selection that are not visible records on this profile. The\n * profile is the owner's own, so an empty result means every pick is theirs.\n */\nexport function findUnknownFunderCvItems(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n): string[] {\n const known = new Set(listFunderCvCandidates(profile, FUNDER_CV_ITEM_KINDS).map((c) => c.id));\n return selectedIds(selection).filter((id) => !known.has(id));\n}\n\n// ---------------------------------------------------------------------------\n// Document shapes\n// ---------------------------------------------------------------------------\n\nexport interface FunderCvPerson {\n /** Full name as the profile shows it; the handle when there is no name. */\n readonly name: string;\n readonly givenName?: string;\n readonly familyName?: string;\n /** Bare ORCID iD, e.g. `0000-0002-1825-0097`. */\n readonly orcid?: string;\n readonly website?: string;\n}\n\n/**\n * The ERC person, with the contact details the owner typed for this export.\n * The NIH Biographical Sketch has no contact block, so only ERC carries them.\n */\nexport interface ErcFunderCvPerson extends FunderCvPerson {\n readonly email?: string;\n readonly phone?: string;\n /** Lines are separated by `\\n`. */\n readonly address?: string;\n}\n\nexport interface FunderCvOptions {\n /**\n * Contact details the owner typed for this export. Sifa stores none of\n * them. Used for ERC only.\n */\n readonly contact?: ExportContact;\n}\n\n/** One product or research output, ready to cite. */\nexport interface FunderCvCitation {\n readonly kind: 'publication' | 'presentation';\n readonly title: string;\n /** Authors in record order, comma separated. For a talk, the speaker. */\n readonly authors?: string;\n /** Journal, publisher or event. */\n readonly venue?: string;\n /** Year. */\n readonly date?: string;\n readonly doi?: string;\n readonly url?: string;\n}\n\nexport interface FunderCvHonor {\n readonly year?: string;\n readonly title: string;\n readonly organization?: string;\n}\n\nexport interface NihEducation {\n readonly organization: string;\n readonly location?: string;\n readonly degree?: string;\n /** Start date, `MM/YYYY` or `YYYY`. */\n readonly start?: string;\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly end?: string;\n readonly field?: string;\n}\n\nexport interface FunderCvAppointment {\n /** `YYYY`. */\n readonly start?: string;\n /** `YYYY`, or null for a current appointment. */\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n readonly location?: string;\n}\n\n/** NIH Biographical Sketch: Common Form plus NIH Supplement. */\nexport interface NihFunderCv {\n readonly format: 'nih';\n readonly person: FunderCvPerson;\n readonly positionTitle?: string;\n /** Primary organization: the current position's. */\n readonly organization?: string;\n readonly location?: string;\n readonly education: NihEducation[];\n readonly appointments: FunderCvAppointment[];\n readonly products: { readonly related: FunderCvCitation[]; readonly other: FunderCvCitation[] };\n readonly honors: FunderCvHonor[];\n /** Supplement \"Personal Statement\". Sifa holds no record for it. */\n readonly personalStatement: string | null;\n /** Supplement \"Contributions to Science\". Sifa holds no record for them. */\n readonly contributions: string[];\n}\n\nexport interface ErcEducation {\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly date?: string;\n readonly degree?: string;\n readonly field?: string;\n readonly institution: string;\n}\n\nexport interface ErcRecognition {\n readonly kind: 'honor' | 'presentation';\n readonly year?: string;\n readonly title: string;\n /** Awarding body, or the event a talk was given at. */\n readonly organization?: string;\n}\n\nexport interface ErcContribution {\n readonly kind: 'teaching' | 'service';\n readonly start?: string;\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n}\n\n/** ERC Part B1, section \"Curriculum vitae and Track Record\". */\nexport interface ErcFunderCv {\n readonly format: 'erc';\n readonly person: ErcFunderCvPerson;\n readonly education: ErcEducation[];\n readonly currentPositions: FunderCvAppointment[];\n readonly previousPositions: FunderCvAppointment[];\n readonly outputs: FunderCvCitation[];\n readonly recognition: ErcRecognition[];\n readonly communityContributions: ErcContribution[];\n /** Proposal acronym for the page header. Not a profile fact. */\n readonly acronym: string | null;\n /** \"Career breaks, diverse career paths and major life events\". */\n readonly careerBreaks: string | null;\n}\n\nexport type FunderCvDocument = NihFunderCv | ErcFunderCv;\n\n// ---------------------------------------------------------------------------\n// Mapping\n// ---------------------------------------------------------------------------\n\nfunction text(value: string | null | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\nfunction year(value: string | null | undefined): string | undefined {\n return toResumeDate(value ?? undefined)?.slice(0, 4);\n}\n\n/** `MM/YYYY` when the month is known, else `YYYY`. */\nfunction monthYear(value: string | null | undefined): string | undefined {\n const date = toResumeDate(value ?? undefined);\n if (!date) return undefined;\n return date.length >= 7 ? `${date.slice(5, 7)}/${date.slice(0, 4)}` : date;\n}\n\n/** Newest start first; undated entries last, in profile order. */\nfunction byStartDesc<T>(items: T[], start: (item: T) => string | undefined): T[] {\n return [...items].sort((a, b) => {\n const da = toResumeDate(start(a));\n const db = toResumeDate(start(b));\n if (da && db) return db.localeCompare(da);\n if (da) return -1;\n if (db) return 1;\n return 0;\n });\n}\n\nconst ORCID_RE = /\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX]/;\n\nfunction orcidOf(accounts: ExternalAccount[] | undefined): string | undefined {\n const account = filterHidden(accounts).find((a) => a.platform.toLowerCase() === 'orcid');\n return account ? ORCID_RE.exec(account.url)?.[0] : undefined;\n}\n\nfunction personOf(profile: FunderCvProfileInput): FunderCvPerson {\n return compact<FunderCvPerson>({\n name:\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName) ??\n profile.handle,\n givenName: text(profile.givenName),\n familyName: text(profile.familyName),\n orcid: orcidOf(profile.externalAccounts),\n website: text(profile.website),\n });\n}\n\nfunction appointment(position: ProfilePosition): FunderCvAppointment {\n return compact<FunderCvAppointment>({\n start: year(position.startedAt),\n end: year(position.endedAt) ?? null,\n title: position.title,\n organization: text(position.entityName) ?? text(position.company),\n location: text(formatLocation(position.location)),\n });\n}\n\n/** The current position to name on the form: the flagged primary, else the newest. */\nfunction currentPosition(positions: ProfilePosition[]): ProfilePosition | undefined {\n const current = byStartDesc(\n positions.filter((p) => !text(p.endedAt)),\n (p) => p.startedAt,\n );\n return current.find((p) => p.primary) ?? current[0];\n}\n\nfunction lookup(profile: FunderCvProfileInput) {\n const publications = new Map(filterHidden(profile.publications).map((p) => [p.rkey, p]));\n const presentations = new Map(filterHidden(profile.presentations).map((p) => [p.rkey, p]));\n const honors = new Map(filterHidden(profile.honors).map((h) => [h.rkey, h]));\n const split = (id: string) => {\n const at = id.indexOf(':');\n return { kind: id.slice(0, at), rkey: id.slice(at + 1) };\n };\n return { publications, presentations, honors, split };\n}\n\nfunction citations(\n ids: readonly string[],\n profile: FunderCvProfileInput,\n person: FunderCvPerson,\n): FunderCvCitation[] {\n const { publications, presentations, split } = lookup(profile);\n return ids.flatMap((id): FunderCvCitation[] => {\n const { kind, rkey } = split(id);\n if (kind === 'publication') {\n const p = publications.get(rkey);\n if (!p) return [];\n const authors = (p.contributors ?? []).map((c) => c.name.trim()).filter(Boolean);\n return [\n compact<FunderCvCitation>({\n kind: 'publication',\n title: p.title,\n authors: authors.length > 0 ? authors.join(', ') : undefined,\n venue: text(p.publisher),\n date: year(p.date),\n doi: text(p.doi),\n url: text(p.url),\n }),\n ];\n }\n if (kind === 'presentation') {\n const talk = presentations.get(rkey);\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<FunderCvCitation>({\n kind: 'presentation',\n title: talk.title,\n authors: person.name,\n venue: text(delivery?.eventName),\n date: year(delivery?.date),\n url: text(talk.links?.[0]?.uri),\n }),\n ];\n }\n return [];\n });\n}\n\nfunction honorsOf(ids: readonly string[], profile: FunderCvProfileInput): FunderCvHonor[] {\n const { honors, split } = lookup(profile);\n return ids.flatMap((id) => {\n const { kind, rkey } = split(id);\n const h = kind === 'honor' ? honors.get(rkey) : undefined;\n if (!h) return [];\n return [\n compact<FunderCvHonor>({\n year: year(h.date),\n title: h.title,\n organization: text(h.entityName) ?? text(h.issuer),\n }),\n ];\n });\n}\n\n/**\n * Build the funder document from the profile and the owner's selection.\n *\n * Hidden records never appear. Selected ids that match no visible record are\n * skipped; callers that must refuse them check `findUnknownFunderCvItems`\n * first. Selected items keep the order they were picked in.\n */\nexport function profileToFunderCv(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n options: FunderCvOptions = {},\n): FunderCvDocument {\n const person = personOf(profile);\n const positions = filterHidden(profile.positions);\n const education = byStartDesc(filterHidden(profile.education), (e) => e.startedAt);\n\n if (selection.format === 'nih') {\n const current = currentPosition(positions);\n return compact<NihFunderCv>({\n format: 'nih',\n person,\n positionTitle: text(current?.title),\n organization: current ? appointment(current).organization : undefined,\n location: text(formatLocation(current?.location)),\n education: education.map((e) =>\n compact<NihEducation>({\n organization: text(e.entityName) ?? e.institution,\n degree: text(e.degree),\n start: monthYear(e.startedAt),\n end: monthYear(e.endedAt),\n field: text(e.fieldOfStudy),\n }),\n ),\n appointments: byStartDesc(positions, (p) => p.startedAt).map(appointment),\n products: {\n related: citations(selection.related, profile, person),\n other: citations(selection.other, profile, person),\n },\n honors: honorsOf(selection.honors, profile),\n personalStatement: null,\n contributions: [],\n });\n }\n\n const appointments = byStartDesc(positions, (p) => p.startedAt);\n const { presentations, split } = lookup(profile);\n\n const recognition = selection.recognition.flatMap((id): ErcRecognition[] => {\n const { kind, rkey } = split(id);\n if (kind === 'honor') {\n const [honor] = honorsOf([id], profile);\n return honor ? [{ kind: 'honor', ...honor }] : [];\n }\n const talk = kind === 'presentation' ? presentations.get(rkey) : undefined;\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<ErcRecognition>({\n kind: 'presentation',\n year: year(delivery?.date),\n title: talk.title,\n organization: text(delivery?.eventName),\n }),\n ];\n });\n\n const teaching = filterHidden(profile.courses)\n .filter(isTeachingCourse)\n .map((c) =>\n compact<ErcContribution>({\n kind: 'teaching',\n start: year(c.startedAt),\n end: year(c.endedAt) ?? null,\n title: c.name,\n organization: text(c.entityName) ?? text(c.institution),\n }),\n );\n const service = filterHidden(profile.volunteering).map((v) =>\n compact<ErcContribution>({\n kind: 'service',\n start: year(v.startDate),\n end: year(v.endDate) ?? null,\n title: text(v.role) ?? text(v.entityName) ?? v.organization,\n organization: text(v.entityName) ?? text(v.organization),\n }),\n );\n\n return {\n format: 'erc',\n person: { ...person, ...normalizeExportContact(options.contact ?? {}) },\n education: education.map((e) =>\n compact<ErcEducation>({\n date: monthYear(e.endedAt) ?? monthYear(e.startedAt),\n degree: text(e.degree),\n field: text(e.fieldOfStudy),\n institution: text(e.entityName) ?? e.institution,\n }),\n ),\n currentPositions: appointments.filter((p) => !text(p.endedAt)).map(appointment),\n previousPositions: appointments.filter((p) => !!text(p.endedAt)).map(appointment),\n outputs: citations(selection.outputs, profile, person),\n recognition,\n communityContributions: [...teaching, ...service],\n acronym: null,\n careerBreaks: null,\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/resume/funder-cv.ts"],"sourcesContent":["/**\n * Funder-format CV documents: the NIH Biographical Sketch (Common Form plus\n * NIH Supplement) and the ERC Part B1 \"Curriculum vitae and Track Record\".\n *\n * A pure mapper from a Sifa profile and the owner's pick of items to a plain\n * JSON document a typesetter lays out. Like the JSON Resume emitter next door,\n * nothing here fetches or renders.\n *\n * Funders cap how many items a CV may list. The caps live in\n * `FUNDER_CV_GROUPS`, so the export page that offers the choice and the server\n * that enforces it read one number. Sections a funder asks for that Sifa holds\n * no records for (a personal statement, contributions to science, career\n * breaks) are emitted as `null` or empty, for the template to show as a\n * placeholder the applicant fills in.\n */\n\nimport { z } from 'zod';\n\nimport { formatLocation } from '../format/location-utils.js';\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport { isTeachingCourse } from '../taxonomy/course-role.js';\nimport type {\n ExternalAccount,\n ProfileCourse,\n ProfileEducation,\n ProfileHonor,\n ProfilePosition,\n ProfilePresentation,\n ProfilePublication,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normalizeExportContact, type ExportContact } from './export-contact.js';\nimport { toResumeDate } from './json-resume.js';\n\nexport const FUNDER_CV_FORMATS = ['nih', 'erc'] as const;\nexport type FunderCvFormat = (typeof FUNDER_CV_FORMATS)[number];\n\nexport const FUNDER_CV_ITEM_KINDS = ['publication', 'presentation', 'honor'] as const;\nexport type FunderCvItemKind = (typeof FUNDER_CV_ITEM_KINDS)[number];\n\nexport interface FunderCvGroupSpec {\n readonly kinds: readonly FunderCvItemKind[];\n /** Most items the funder's form takes for this group. */\n readonly max: number;\n}\n\n/**\n * The pickable groups per format, with the funder's item limits.\n *\n * - NIH Common Form \"Products\": up to 5 closely related to the proposed\n * project and up to 5 other significant products. Publications, conference\n * papers and presentations all count. NIH Supplement \"Honors\": up to 15.\n * - ERC Part B1 \"Research achievements\": up to ten research outputs. \"Peer\n * recognition\" is \"selected examples\" with no number; 15 is Sifa's own cap,\n * to keep the section inside the 4-page limit.\n */\nexport const FUNDER_CV_GROUPS = {\n nih: {\n related: { kinds: ['publication', 'presentation'], max: 5 },\n other: { kinds: ['publication', 'presentation'], max: 5 },\n honors: { kinds: ['honor'], max: 15 },\n },\n erc: {\n outputs: { kinds: ['publication'], max: 10 },\n recognition: { kinds: ['honor', 'presentation'], max: 15 },\n },\n} as const satisfies Record<FunderCvFormat, Record<string, FunderCvGroupSpec>>;\n\nexport type FunderCvGroupName<F extends FunderCvFormat> = keyof (typeof FUNDER_CV_GROUPS)[F] &\n string;\n\n/** A picked item: `<kind>:<rkey>`, e.g. `publication:3kxyz`. */\nexport function funderCvItemId(kind: FunderCvItemKind, rkey: string): string {\n return `${kind}:${rkey}`;\n}\n\n/** Record key syntax (atproto): 1-512 of A-Z a-z 0-9 . - _ : ~ */\nconst ITEM_ID_RE = /^(publication|presentation|honor):[A-Za-z0-9._~:-]{1,512}$/;\n\nfunction kindOf(id: string): FunderCvItemKind | undefined {\n const kind = id.slice(0, id.indexOf(':'));\n return (FUNDER_CV_ITEM_KINDS as readonly string[]).includes(kind)\n ? (kind as FunderCvItemKind)\n : undefined;\n}\n\nfunction groupSchema(spec: FunderCvGroupSpec) {\n return z\n .array(\n z\n .string()\n .regex(ITEM_ID_RE)\n .refine((id) => spec.kinds.includes(kindOf(id) as FunderCvItemKind), {\n message: `Only ${spec.kinds.join(', ')} items fit here`,\n }),\n )\n .max(spec.max)\n .default([]);\n}\n\nfunction noRepeats(lists: readonly string[][]): boolean {\n const all = lists.flat();\n return new Set(all).size === all.length;\n}\n\nconst nihSelectionSchema = z\n .object({\n format: z.literal('nih'),\n related: groupSchema(FUNDER_CV_GROUPS.nih.related),\n other: groupSchema(FUNDER_CV_GROUPS.nih.other),\n honors: groupSchema(FUNDER_CV_GROUPS.nih.honors),\n })\n .refine((s) => noRepeats([s.related, s.other, s.honors]), {\n message: 'An item can be listed once',\n });\n\nconst ercSelectionSchema = z\n .object({\n format: z.literal('erc'),\n outputs: groupSchema(FUNDER_CV_GROUPS.erc.outputs),\n recognition: groupSchema(FUNDER_CV_GROUPS.erc.recognition),\n })\n .refine((s) => noRepeats([s.outputs, s.recognition]), {\n message: 'An item can be listed once',\n });\n\n/**\n * The owner's pick for one format. Enforces the shape and the funder's limits;\n * whether each id is one of the owner's own visible records is a separate\n * check against the profile, `findUnknownFunderCvItems`.\n */\nexport const funderCvSelectionSchema = z.discriminatedUnion('format', [\n nihSelectionSchema,\n ercSelectionSchema,\n]);\n\nexport type FunderCvSelection = z.infer<typeof funderCvSelectionSchema>;\n\n/** The subset of `Profile` the funder mapper reads. */\nexport interface FunderCvProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly headline?: string;\n readonly website?: string;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly publications?: ProfilePublication[];\n readonly presentations?: ProfilePresentation[];\n readonly honors?: ProfileHonor[];\n readonly courses?: ProfileCourse[];\n readonly volunteering?: ProfileVolunteering[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface FunderCvCandidate {\n readonly id: string;\n readonly kind: FunderCvItemKind;\n readonly title: string;\n /** `YYYY`, `YYYY-MM` or `YYYY-MM-DD`; absent when the record has no date. */\n readonly date?: string;\n}\n\n/** The most recent visible delivery of a talk, which dates it. */\nfunction latestDeliveryRecord(presentation: ProfilePresentation) {\n const dateOf = (d: { date?: string | null }) => toResumeDate(d.date ?? undefined) ?? '';\n return [...filterHidden(presentation.deliveries)]\n .sort((a, b) => dateOf(a).localeCompare(dateOf(b)))\n .at(-1);\n}\n\nfunction latestDelivery(presentation: ProfilePresentation): string | undefined {\n return toResumeDate(latestDeliveryRecord(presentation)?.date ?? undefined);\n}\n\nfunction candidatesOf(profile: FunderCvProfileInput, kind: FunderCvItemKind): FunderCvCandidate[] {\n switch (kind) {\n case 'publication':\n return filterHidden(profile.publications).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: toResumeDate(p.date),\n }));\n case 'presentation':\n return filterHidden(profile.presentations).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: latestDelivery(p),\n }));\n case 'honor':\n return filterHidden(profile.honors).map((h) => ({\n id: funderCvItemId(kind, h.rkey),\n kind,\n title: h.title,\n date: toResumeDate(h.date),\n }));\n }\n}\n\n/**\n * The owner's visible items of the given kinds, most recent first. Undated\n * items go last, in profile order. The sort is stable, so equal dates keep\n * profile order too.\n */\nexport function listFunderCvCandidates(\n profile: FunderCvProfileInput,\n kinds: readonly FunderCvItemKind[],\n): FunderCvCandidate[] {\n const all = kinds.flatMap((kind) => candidatesOf(profile, kind));\n return all.sort((a, b) => {\n if (a.date && b.date) return b.date.localeCompare(a.date);\n if (a.date) return -1;\n if (b.date) return 1;\n return 0;\n });\n}\n\n/**\n * What the export page pre-ticks: the most recent publications up to each\n * limit (for NIH, the first five as closely related and the next five as\n * other), and the most recent honors. Talks are offered but not pre-ticked.\n */\nexport function defaultFunderCvSelection(\n profile: FunderCvProfileInput,\n format: FunderCvFormat,\n): FunderCvSelection {\n const publications = listFunderCvCandidates(profile, ['publication']).map((c) => c.id);\n const honors = listFunderCvCandidates(profile, ['honor']).map((c) => c.id);\n if (format === 'nih') {\n const { related, other } = FUNDER_CV_GROUPS.nih;\n return {\n format,\n related: publications.slice(0, related.max),\n other: publications.slice(related.max, related.max + other.max),\n honors: honors.slice(0, FUNDER_CV_GROUPS.nih.honors.max),\n };\n }\n return {\n format,\n outputs: publications.slice(0, FUNDER_CV_GROUPS.erc.outputs.max),\n recognition: honors.slice(0, FUNDER_CV_GROUPS.erc.recognition.max),\n };\n}\n\nfunction selectedIds(selection: FunderCvSelection): string[] {\n return selection.format === 'nih'\n ? [...selection.related, ...selection.other, ...selection.honors]\n : [...selection.outputs, ...selection.recognition];\n}\n\n/**\n * Ids in the selection that are not visible records on this profile. The\n * profile is the owner's own, so an empty result means every pick is theirs.\n */\nexport function findUnknownFunderCvItems(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n): string[] {\n const known = new Set(listFunderCvCandidates(profile, FUNDER_CV_ITEM_KINDS).map((c) => c.id));\n return selectedIds(selection).filter((id) => !known.has(id));\n}\n\n// ---------------------------------------------------------------------------\n// Document shapes\n// ---------------------------------------------------------------------------\n\nexport interface FunderCvPerson {\n /** Full name as the profile shows it; the handle when there is no name. */\n readonly name: string;\n readonly givenName?: string;\n readonly familyName?: string;\n /** Bare ORCID iD, e.g. `0000-0002-1825-0097`. */\n readonly orcid?: string;\n readonly website?: string;\n}\n\n/**\n * The ERC person, with the contact and personal details the owner typed for\n * this export. The NIH Biographical Sketch has no such block, so only ERC carries them.\n */\nexport interface ErcFunderCvPerson extends FunderCvPerson {\n readonly email?: string;\n readonly phone?: string;\n /** Lines are separated by `\\n`. */\n readonly address?: string;\n readonly driversLicense?: string;\n readonly rightToWork?: string;\n readonly nationality?: string;\n}\n\nexport interface FunderCvOptions {\n /**\n * Contact details the owner typed for this export. Sifa stores none of\n * them. Used for ERC only.\n */\n readonly contact?: ExportContact;\n}\n\n/** One product or research output, ready to cite. */\nexport interface FunderCvCitation {\n readonly kind: 'publication' | 'presentation';\n readonly title: string;\n /** Authors in record order, comma separated. For a talk, the speaker. */\n readonly authors?: string;\n /** Journal, publisher or event. */\n readonly venue?: string;\n /** Year. */\n readonly date?: string;\n readonly doi?: string;\n readonly url?: string;\n}\n\nexport interface FunderCvHonor {\n readonly year?: string;\n readonly title: string;\n readonly organization?: string;\n}\n\nexport interface NihEducation {\n readonly organization: string;\n readonly location?: string;\n readonly degree?: string;\n /** Start date, `MM/YYYY` or `YYYY`. */\n readonly start?: string;\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly end?: string;\n readonly field?: string;\n}\n\nexport interface FunderCvAppointment {\n /** `YYYY`. */\n readonly start?: string;\n /** `YYYY`, or null for a current appointment. */\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n readonly location?: string;\n}\n\n/** NIH Biographical Sketch: Common Form plus NIH Supplement. */\nexport interface NihFunderCv {\n readonly format: 'nih';\n readonly person: FunderCvPerson;\n readonly positionTitle?: string;\n /** Primary organization: the current position's. */\n readonly organization?: string;\n readonly location?: string;\n readonly education: NihEducation[];\n readonly appointments: FunderCvAppointment[];\n readonly products: { readonly related: FunderCvCitation[]; readonly other: FunderCvCitation[] };\n readonly honors: FunderCvHonor[];\n /** Supplement \"Personal Statement\". Sifa holds no record for it. */\n readonly personalStatement: string | null;\n /** Supplement \"Contributions to Science\". Sifa holds no record for them. */\n readonly contributions: string[];\n}\n\nexport interface ErcEducation {\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly date?: string;\n readonly degree?: string;\n readonly field?: string;\n readonly institution: string;\n}\n\nexport interface ErcRecognition {\n readonly kind: 'honor' | 'presentation';\n readonly year?: string;\n readonly title: string;\n /** Awarding body, or the event a talk was given at. */\n readonly organization?: string;\n}\n\nexport interface ErcContribution {\n readonly kind: 'teaching' | 'service';\n readonly start?: string;\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n}\n\n/** ERC Part B1, section \"Curriculum vitae and Track Record\". */\nexport interface ErcFunderCv {\n readonly format: 'erc';\n readonly person: ErcFunderCvPerson;\n readonly education: ErcEducation[];\n readonly currentPositions: FunderCvAppointment[];\n readonly previousPositions: FunderCvAppointment[];\n readonly outputs: FunderCvCitation[];\n readonly recognition: ErcRecognition[];\n readonly communityContributions: ErcContribution[];\n /** Proposal acronym for the page header. Not a profile fact. */\n readonly acronym: string | null;\n /** \"Career breaks, diverse career paths and major life events\". */\n readonly careerBreaks: string | null;\n}\n\nexport type FunderCvDocument = NihFunderCv | ErcFunderCv;\n\n// ---------------------------------------------------------------------------\n// Mapping\n// ---------------------------------------------------------------------------\n\nfunction text(value: string | null | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\nfunction year(value: string | null | undefined): string | undefined {\n return toResumeDate(value ?? undefined)?.slice(0, 4);\n}\n\n/** `MM/YYYY` when the month is known, else `YYYY`. */\nfunction monthYear(value: string | null | undefined): string | undefined {\n const date = toResumeDate(value ?? undefined);\n if (!date) return undefined;\n return date.length >= 7 ? `${date.slice(5, 7)}/${date.slice(0, 4)}` : date;\n}\n\n/** Newest start first; undated entries last, in profile order. */\nfunction byStartDesc<T>(items: T[], start: (item: T) => string | undefined): T[] {\n return [...items].sort((a, b) => {\n const da = toResumeDate(start(a));\n const db = toResumeDate(start(b));\n if (da && db) return db.localeCompare(da);\n if (da) return -1;\n if (db) return 1;\n return 0;\n });\n}\n\nconst ORCID_RE = /\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX]/;\n\nfunction orcidOf(accounts: ExternalAccount[] | undefined): string | undefined {\n const account = filterHidden(accounts).find((a) => a.platform.toLowerCase() === 'orcid');\n return account ? ORCID_RE.exec(account.url)?.[0] : undefined;\n}\n\nfunction personOf(profile: FunderCvProfileInput): FunderCvPerson {\n return compact<FunderCvPerson>({\n name:\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName) ??\n profile.handle,\n givenName: text(profile.givenName),\n familyName: text(profile.familyName),\n orcid: orcidOf(profile.externalAccounts),\n website: text(profile.website),\n });\n}\n\nfunction appointment(position: ProfilePosition): FunderCvAppointment {\n return compact<FunderCvAppointment>({\n start: year(position.startedAt),\n end: year(position.endedAt) ?? null,\n title: position.title,\n organization: text(position.entityName) ?? text(position.company),\n location: text(formatLocation(position.location)),\n });\n}\n\n/** The current position to name on the form: the flagged primary, else the newest. */\nfunction currentPosition(positions: ProfilePosition[]): ProfilePosition | undefined {\n const current = byStartDesc(\n positions.filter((p) => !text(p.endedAt)),\n (p) => p.startedAt,\n );\n return current.find((p) => p.primary) ?? current[0];\n}\n\nfunction lookup(profile: FunderCvProfileInput) {\n const publications = new Map(filterHidden(profile.publications).map((p) => [p.rkey, p]));\n const presentations = new Map(filterHidden(profile.presentations).map((p) => [p.rkey, p]));\n const honors = new Map(filterHidden(profile.honors).map((h) => [h.rkey, h]));\n const split = (id: string) => {\n const at = id.indexOf(':');\n return { kind: id.slice(0, at), rkey: id.slice(at + 1) };\n };\n return { publications, presentations, honors, split };\n}\n\nfunction citations(\n ids: readonly string[],\n profile: FunderCvProfileInput,\n person: FunderCvPerson,\n): FunderCvCitation[] {\n const { publications, presentations, split } = lookup(profile);\n return ids.flatMap((id): FunderCvCitation[] => {\n const { kind, rkey } = split(id);\n if (kind === 'publication') {\n const p = publications.get(rkey);\n if (!p) return [];\n const authors = (p.contributors ?? []).map((c) => c.name.trim()).filter(Boolean);\n return [\n compact<FunderCvCitation>({\n kind: 'publication',\n title: p.title,\n authors: authors.length > 0 ? authors.join(', ') : undefined,\n venue: text(p.publisher),\n date: year(p.date),\n doi: text(p.doi),\n url: text(p.url),\n }),\n ];\n }\n if (kind === 'presentation') {\n const talk = presentations.get(rkey);\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<FunderCvCitation>({\n kind: 'presentation',\n title: talk.title,\n authors: person.name,\n venue: text(delivery?.eventName),\n date: year(delivery?.date),\n url: text(talk.links?.[0]?.uri),\n }),\n ];\n }\n return [];\n });\n}\n\nfunction honorsOf(ids: readonly string[], profile: FunderCvProfileInput): FunderCvHonor[] {\n const { honors, split } = lookup(profile);\n return ids.flatMap((id) => {\n const { kind, rkey } = split(id);\n const h = kind === 'honor' ? honors.get(rkey) : undefined;\n if (!h) return [];\n return [\n compact<FunderCvHonor>({\n year: year(h.date),\n title: h.title,\n organization: text(h.entityName) ?? text(h.issuer),\n }),\n ];\n });\n}\n\n/**\n * Build the funder document from the profile and the owner's selection.\n *\n * Hidden records never appear. Selected ids that match no visible record are\n * skipped; callers that must refuse them check `findUnknownFunderCvItems`\n * first. Selected items keep the order they were picked in.\n */\nexport function profileToFunderCv(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n options: FunderCvOptions = {},\n): FunderCvDocument {\n const person = personOf(profile);\n const positions = filterHidden(profile.positions);\n const education = byStartDesc(filterHidden(profile.education), (e) => e.startedAt);\n\n if (selection.format === 'nih') {\n const current = currentPosition(positions);\n return compact<NihFunderCv>({\n format: 'nih',\n person,\n positionTitle: text(current?.title),\n organization: current ? appointment(current).organization : undefined,\n location: text(formatLocation(current?.location)),\n education: education.map((e) =>\n compact<NihEducation>({\n organization: text(e.entityName) ?? e.institution,\n degree: text(e.degree),\n start: monthYear(e.startedAt),\n end: monthYear(e.endedAt),\n field: text(e.fieldOfStudy),\n }),\n ),\n appointments: byStartDesc(positions, (p) => p.startedAt).map(appointment),\n products: {\n related: citations(selection.related, profile, person),\n other: citations(selection.other, profile, person),\n },\n honors: honorsOf(selection.honors, profile),\n personalStatement: null,\n contributions: [],\n });\n }\n\n const appointments = byStartDesc(positions, (p) => p.startedAt);\n const { presentations, split } = lookup(profile);\n\n const recognition = selection.recognition.flatMap((id): ErcRecognition[] => {\n const { kind, rkey } = split(id);\n if (kind === 'honor') {\n const [honor] = honorsOf([id], profile);\n return honor ? [{ kind: 'honor', ...honor }] : [];\n }\n const talk = kind === 'presentation' ? presentations.get(rkey) : undefined;\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<ErcRecognition>({\n kind: 'presentation',\n year: year(delivery?.date),\n title: talk.title,\n organization: text(delivery?.eventName),\n }),\n ];\n });\n\n const teaching = filterHidden(profile.courses)\n .filter(isTeachingCourse)\n .map((c) =>\n compact<ErcContribution>({\n kind: 'teaching',\n start: year(c.startedAt),\n end: year(c.endedAt) ?? null,\n title: c.name,\n organization: text(c.entityName) ?? text(c.institution),\n }),\n );\n const service = filterHidden(profile.volunteering).map((v) =>\n compact<ErcContribution>({\n kind: 'service',\n start: year(v.startDate),\n end: year(v.endDate) ?? null,\n title: text(v.role) ?? text(v.entityName) ?? v.organization,\n organization: text(v.entityName) ?? text(v.organization),\n }),\n );\n\n return {\n format: 'erc',\n person: { ...person, ...normalizeExportContact(options.contact ?? {}) },\n education: education.map((e) =>\n compact<ErcEducation>({\n date: monthYear(e.endedAt) ?? monthYear(e.startedAt),\n degree: text(e.degree),\n field: text(e.fieldOfStudy),\n institution: text(e.entityName) ?? e.institution,\n }),\n ),\n currentPositions: appointments.filter((p) => !text(p.endedAt)).map(appointment),\n previousPositions: appointments.filter((p) => !!text(p.endedAt)).map(appointment),\n outputs: citations(selection.outputs, profile, person),\n recognition,\n communityContributions: [...teaching, ...service],\n acronym: null,\n careerBreaks: null,\n };\n}\n"],"mappings":"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","names":[]}
|
package/dist/resume/index.cjs
CHANGED
|
@@ -6,28 +6,43 @@ var zod = require('zod');
|
|
|
6
6
|
var EXPORT_CONTACT_LIMITS = {
|
|
7
7
|
email: 254,
|
|
8
8
|
phone: 40,
|
|
9
|
-
address: 500
|
|
9
|
+
address: 500,
|
|
10
|
+
driversLicense: 60,
|
|
11
|
+
rightToWork: 200,
|
|
12
|
+
nationality: 100
|
|
10
13
|
};
|
|
11
14
|
var exportContactSchema = zod.z.strictObject({
|
|
12
15
|
email: zod.z.string().max(EXPORT_CONTACT_LIMITS.email).optional(),
|
|
13
16
|
phone: zod.z.string().max(EXPORT_CONTACT_LIMITS.phone).optional(),
|
|
14
|
-
address: zod.z.string().max(EXPORT_CONTACT_LIMITS.address).optional()
|
|
17
|
+
address: zod.z.string().max(EXPORT_CONTACT_LIMITS.address).optional(),
|
|
18
|
+
driversLicense: zod.z.string().max(EXPORT_CONTACT_LIMITS.driversLicense).optional(),
|
|
19
|
+
rightToWork: zod.z.string().max(EXPORT_CONTACT_LIMITS.rightToWork).optional(),
|
|
20
|
+
nationality: zod.z.string().max(EXPORT_CONTACT_LIMITS.nationality).optional()
|
|
15
21
|
});
|
|
16
22
|
function pickExportContact(draft) {
|
|
17
23
|
return normalizeExportContact({
|
|
18
24
|
email: draft.includeEmail ? draft.email : void 0,
|
|
19
25
|
phone: draft.includePhone ? draft.phone : void 0,
|
|
20
|
-
address: draft.includeAddress ? draft.address : void 0
|
|
26
|
+
address: draft.includeAddress ? draft.address : void 0,
|
|
27
|
+
driversLicense: draft.includeDriversLicense ? draft.driversLicense : void 0,
|
|
28
|
+
rightToWork: draft.includeRightToWork ? draft.rightToWork : void 0,
|
|
29
|
+
nationality: draft.includeNationality ? draft.nationality : void 0
|
|
21
30
|
});
|
|
22
31
|
}
|
|
23
32
|
function normalizeExportContact(contact) {
|
|
24
33
|
const email = cap(oneLine(contact.email), EXPORT_CONTACT_LIMITS.email);
|
|
25
34
|
const phone = cap(oneLine(contact.phone), EXPORT_CONTACT_LIMITS.phone);
|
|
26
35
|
const address = cap(lines(contact.address), EXPORT_CONTACT_LIMITS.address);
|
|
36
|
+
const driversLicense = cap(oneLine(contact.driversLicense), EXPORT_CONTACT_LIMITS.driversLicense);
|
|
37
|
+
const rightToWork = cap(oneLine(contact.rightToWork), EXPORT_CONTACT_LIMITS.rightToWork);
|
|
38
|
+
const nationality = cap(oneLine(contact.nationality), EXPORT_CONTACT_LIMITS.nationality);
|
|
27
39
|
return {
|
|
28
40
|
...email ? { email } : {},
|
|
29
41
|
...phone ? { phone } : {},
|
|
30
|
-
...address ? { address } : {}
|
|
42
|
+
...address ? { address } : {},
|
|
43
|
+
...driversLicense ? { driversLicense } : {},
|
|
44
|
+
...rightToWork ? { rightToWork } : {},
|
|
45
|
+
...nationality ? { nationality } : {}
|
|
31
46
|
};
|
|
32
47
|
}
|
|
33
48
|
function looksLikeEmail(value) {
|
|
@@ -40,7 +55,10 @@ function looksLikeEmail(value) {
|
|
|
40
55
|
return local.length > 0 && dot > 0 && dot < domain.length - 1;
|
|
41
56
|
}
|
|
42
57
|
function cap(value, max) {
|
|
43
|
-
|
|
58
|
+
if (value === void 0) return void 0;
|
|
59
|
+
let cut = value.slice(0, max);
|
|
60
|
+
if (/[\uD800-\uDBFF]$/.test(cut)) cut = cut.slice(0, -1);
|
|
61
|
+
return cut.trimEnd();
|
|
44
62
|
}
|
|
45
63
|
function oneLine(value) {
|
|
46
64
|
const collapsed = value?.replace(/\s+/g, " ").trim();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/resume/export-contact.ts","../../src/format/pds-utils.ts","../../src/profile/section-model.ts","../../src/jsonld/url.ts","../../src/resume/json-resume.ts","../../src/format/location-utils.ts","../../src/taxonomy/course-role.ts","../../src/resume/funder-cv.ts"],"names":["z","text","compact"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Private contact details for exports: email, phone and postal address.\n *\n * Sifa never stores these and never writes them to the owner's PDS, where they\n * would be public. The owner types them on the export page, the browser keeps\n * them, and they go only into the files the owner downloads. This module holds\n * the shape, the limits and the pick rule, so the export page and the server\n * that builds a funder CV agree on them.\n */\n\nimport { z } from 'zod';\n\n/** Longest value accepted per field. 254 is the longest valid email address. */\nexport const EXPORT_CONTACT_LIMITS = {\n email: 254,\n phone: 40,\n address: 500,\n} as const;\n\n/** The contact details that go into an export. Absent keys are left out. */\nexport interface ExportContact {\n readonly email?: string;\n readonly phone?: string;\n /** Free text; lines are separated by `\\n`. */\n readonly address?: string;\n}\n\n/** What the owner has typed, plus which fields they chose to include. */\nexport interface ExportContactDraft {\n readonly email?: string;\n readonly phone?: string;\n readonly address?: string;\n readonly includeEmail?: boolean;\n readonly includePhone?: boolean;\n readonly includeAddress?: boolean;\n}\n\n/**\n * Validates a picked contact. Only lengths are checked: phone numbers and\n * addresses vary too much between countries, and a wrong-looking email is a\n * warning for the owner, not a reason to refuse the export.\n */\nexport const exportContactSchema = z.strictObject({\n email: z.string().max(EXPORT_CONTACT_LIMITS.email).optional(),\n phone: z.string().max(EXPORT_CONTACT_LIMITS.phone).optional(),\n address: z.string().max(EXPORT_CONTACT_LIMITS.address).optional(),\n});\n\n/**\n * The fields to put in an export: the included ones that are not blank, run\n * through {@link normalizeExportContact}.\n */\nexport function pickExportContact(draft: ExportContactDraft): ExportContact {\n return normalizeExportContact({\n email: draft.includeEmail ? draft.email : undefined,\n phone: draft.includePhone ? draft.phone : undefined,\n address: draft.includeAddress ? draft.address : undefined,\n });\n}\n\n/**\n * Drops blank fields and tidies the rest. Email and phone become one line with\n * single spaces. An address keeps its line breaks but loses blank lines and the\n * spaces around each line. Each field is cut at its {@link EXPORT_CONTACT_LIMITS}\n * length. The emitters call this too, so a caller that skips\n * {@link pickExportContact} and the schema gets the same bounded output.\n */\nexport function normalizeExportContact(contact: ExportContact): ExportContact {\n const email = cap(oneLine(contact.email), EXPORT_CONTACT_LIMITS.email);\n const phone = cap(oneLine(contact.phone), EXPORT_CONTACT_LIMITS.phone);\n const address = cap(lines(contact.address), EXPORT_CONTACT_LIMITS.address);\n return {\n ...(email ? { email } : {}),\n ...(phone ? { phone } : {}),\n ...(address ? { address } : {}),\n };\n}\n\n/** A loose shape check for a non-blocking \"check this address\" hint. */\nexport function looksLikeEmail(value: string): boolean {\n const trimmed = value.trim();\n if (/\\s/.test(trimmed)) return false;\n const parts = trimmed.split('@');\n if (parts.length !== 2) return false;\n const [local, domain] = parts as [string, string];\n const dot = domain.lastIndexOf('.');\n return local.length > 0 && dot > 0 && dot < domain.length - 1;\n}\n\nfunction cap(value: string | undefined, max: number): string | undefined {\n return value === undefined ? undefined : value.slice(0, max).trimEnd();\n}\n\nfunction oneLine(value: string | undefined): string | undefined {\n const collapsed = value?.replace(/\\s+/g, ' ').trim();\n return collapsed ? collapsed : undefined;\n}\n\nfunction lines(value: string | undefined): string | undefined {\n const kept = (value ?? '')\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean);\n return kept.length > 0 ? kept.join('\\n') : undefined;\n}\n","import type { PdsProviderInfo } from '../types/index.js';\n\nexport interface PdsProvider {\n name: string;\n profileUrl: string;\n host?: string;\n}\n\nconst BSKY_PROFILE_BASE = 'https://bsky.app/profile/';\n\nconst PDS_PROVIDERS: { suffix: string; name: string }[] = [\n { suffix: '.bsky.social', name: 'bluesky' },\n { suffix: '.blacksky.app', name: 'blacksky' },\n { suffix: '.eurosky.social', name: 'eurosky' },\n { suffix: '.northsky.social', name: 'northsky' },\n];\n\nconst KNOWN_PROVIDER_NAMES = new Set(PDS_PROVIDERS.map((p) => p.name));\n\nconst ICON_ONLY_PROVIDERS = new Set(['selfhosted-social', 'selfhosted']);\n\nexport function pdsProviderFromApi(\n apiProvider: PdsProviderInfo | null | undefined,\n handle: string,\n): PdsProvider | null {\n if (!apiProvider) return null;\n if (ICON_ONLY_PROVIDERS.has(apiProvider.name)) {\n return { name: apiProvider.name, profileUrl: '', host: apiProvider.host };\n }\n if (!KNOWN_PROVIDER_NAMES.has(apiProvider.name)) return null;\n return {\n name: apiProvider.name,\n profileUrl: `${BSKY_PROFILE_BASE}${handle}`,\n host: apiProvider.host,\n };\n}\n\nexport function getHandleStem(handle: string): string {\n const lower = handle.toLowerCase();\n for (const provider of PDS_PROVIDERS) {\n if (lower.endsWith(provider.suffix) && lower.length > provider.suffix.length) {\n return handle.slice(0, -provider.suffix.length);\n }\n }\n return handle;\n}\n\nexport function getDisplayLabel(displayName: string | undefined, handle: string): string {\n if (displayName) return displayName;\n return getHandleStem(handle);\n}\n\n/**\n * Combine optional structured name fields (`id.sifa.profile.self.givenName`,\n * `familyName`) into a single string in Schema.org `Person` order — given\n * before family. Returns `undefined` when both are absent so callers can\n * fall back to `displayName`. Whitespace-only inputs are treated as absent.\n * Does not impose cultural ordering: users whose preferred presentation\n * differs (e.g. Eastern name order) should set `displayName` accordingly,\n * which this helper deliberately does not consult.\n */\nexport function formatStructuredName(\n givenName: string | undefined,\n familyName: string | undefined,\n): string | undefined {\n const given = givenName?.trim();\n const family = familyName?.trim();\n if (given && family) return `${given} ${family}`;\n return given || family || undefined;\n}\n\nconst PDS_DISPLAY_NAMES: Record<string, string> = {\n bluesky: 'Bluesky',\n blacksky: 'BlackSky',\n eurosky: 'EuroSky',\n northsky: 'NorthSky',\n 'selfhosted-social': 'Self-hosted',\n selfhosted: 'Self-hosted',\n};\n\nexport function getPdsDisplayName(providerName: string): string {\n return PDS_DISPLAY_NAMES[providerName] ?? providerName;\n}\n\nexport function detectPdsProvider(handle: string): PdsProvider | null {\n const lower = handle.toLowerCase();\n for (const provider of PDS_PROVIDERS) {\n if (lower.endsWith(provider.suffix)) {\n return {\n name: provider.name,\n profileUrl: `${BSKY_PROFILE_BASE}${handle}`,\n };\n }\n }\n return null;\n}\n","/**\n * Shared, single-source-of-truth model for which profile sections render, in\n * what order, their nav grouping, and which items a given viewer may see.\n *\n * Every Sifa profile surface builds on this: the HTML profile page, the\n * Markdown / DOCX / print exports, and the standalone personal-site renderer.\n * Adding a section to {@link ALL_SECTIONS} makes it appear everywhere (the\n * exports drive presence + order from here, and {@link SECTION_LABELS} is an\n * exhaustive `Record<SectionId, string>`, so a new id is a compile error until\n * it has a label).\n *\n * The per-section sorts live in `./section-sorts.js`, built on the same SDK\n * date extractors the HTML section components use, so the surfaces cannot\n * diverge on ordering.\n */\n\nimport type { Profile } from '../types/index.js';\n\n/**\n * Side-nav groups, in display order. Each labels a contiguous run of sections\n * in {@link ALL_SECTIONS}. `labelKey` resolves against the consumer's `sections`\n * i18n namespace.\n */\nexport const SECTION_GROUPS = [\n { id: 'overview', labelKey: 'groupOverview' },\n { id: 'experience', labelKey: 'groupExperience' },\n { id: 'qualifications', labelKey: 'groupQualifications' },\n { id: 'more', labelKey: 'groupMore' },\n] as const;\n\nexport type SectionGroupId = (typeof SECTION_GROUPS)[number]['id'];\n\n/**\n * Profile body sections in render + nav order (evidence-first): identity and\n * activity lead, then living work (career, projects, talks, publications, then\n * skills), then the formal record (credentials, education, courses), then the\n * rest. `ns` selects the consumer's i18n namespace for `labelKey`; `group`\n * ties the section to a {@link SECTION_GROUPS} header.\n */\nexport const ALL_SECTIONS = [\n { id: 'about', labelKey: 'about', ns: 'profile' as const, group: 'overview' as const },\n { id: 'career', labelKey: 'career', ns: 'sections' as const, group: 'experience' as const },\n { id: 'projects', labelKey: 'projects', ns: 'sections' as const, group: 'experience' as const },\n {\n id: 'presentations',\n labelKey: 'talksAndSessions',\n ns: 'sections' as const,\n group: 'experience' as const,\n },\n {\n id: 'publications',\n labelKey: 'publications',\n ns: 'sections' as const,\n group: 'experience' as const,\n },\n { id: 'skills', labelKey: 'skills', ns: 'sections' as const, group: 'experience' as const },\n {\n id: 'credentials',\n labelKey: 'credentials',\n ns: 'sections' as const,\n group: 'qualifications' as const,\n },\n {\n id: 'education',\n labelKey: 'education',\n ns: 'sections' as const,\n group: 'qualifications' as const,\n },\n { id: 'courses', labelKey: 'courses', ns: 'sections' as const, group: 'qualifications' as const },\n { id: 'awards', labelKey: 'awards', ns: 'sections' as const, group: 'more' as const },\n { id: 'involvement', labelKey: 'involvement', ns: 'sections' as const, group: 'more' as const },\n // Its own section rather than a group under involvement: that record is about\n // work contributed, this one about capital deployed.\n { id: 'investments', labelKey: 'investments', ns: 'sections' as const, group: 'more' as const },\n { id: 'languages', labelKey: 'languages', ns: 'sections' as const, group: 'more' as const },\n {\n id: 'other-profiles',\n labelKey: 'otherProfiles',\n ns: 'sections' as const,\n group: 'more' as const,\n },\n] as const;\n\n/** Every profile section id, in canonical render order. */\nexport type SectionId = (typeof ALL_SECTIONS)[number]['id'];\n\n/**\n * Whether a section has content a visitor can see (used to hide empty nav\n * entries and sections for visitors). Items the owner hid do not count: a\n * section whose only entries are hidden renders nothing for a visitor, so it\n * must not get a nav entry either.\n */\nexport function isSectionPopulated(profile: Profile, id: string): boolean {\n const hasVisible = <T extends { hidden?: boolean }>(items: T[] | undefined): boolean =>\n filterHidden(items).length > 0;\n switch (id) {\n case 'about':\n // Text fields, not hideable items. Without a headline the summary shows\n // in the identity card instead, so the About section stays out of it.\n return Boolean(profile.about && profile.headline);\n case 'career':\n return hasVisible(profile.positions);\n case 'education':\n return hasVisible(profile.education);\n case 'courses':\n return hasVisible(profile.courses);\n case 'skills':\n // Skills have no owner-hide flag (`ProfileSkill` carries no `hidden`).\n return Boolean(profile.skills?.length);\n case 'projects':\n return hasVisible(profile.projects);\n case 'credentials':\n return hasVisible(profile.certifications);\n case 'publications':\n return hasVisible(profile.publications);\n case 'presentations':\n return hasVisible(profile.presentations) || hasVisible(profile.presentationDeliveries);\n case 'involvement':\n return hasVisible(profile.involvement);\n case 'investments':\n return hasVisible(profile.investments);\n case 'awards':\n return hasVisible(profile.honors);\n case 'languages':\n return hasVisible(profile.languages);\n case 'other-profiles':\n return hasVisible(profile.externalAccounts);\n default:\n return false;\n }\n}\n\n/**\n * The section ids a given viewer should see, in {@link ALL_SECTIONS} order.\n * Owners see every section; visitors only see populated ones.\n */\nexport function getVisibleSectionIds(profile: Profile, isOwnProfile: boolean): SectionId[] {\n return ALL_SECTIONS.map((s) => s.id).filter(\n (id) => isOwnProfile || isSectionPopulated(profile, id),\n );\n}\n\n/** Drop items the owner has hidden from visitors. */\nexport function filterHidden<T extends { hidden?: boolean }>(items: T[] | undefined): T[] {\n return (items ?? []).filter((i) => !i.hidden);\n}\n\n/**\n * Items visible to a given viewer: owners see their own hidden items, visitors\n * do not. Shared by every export format (markdown, docx, print) and the public\n * `.md` route so the hidden rule lives in one place.\n */\nexport function visibleItems<T extends { hidden?: boolean }>(\n items: T[] | undefined,\n isOwnProfile: boolean,\n): T[] {\n return isOwnProfile ? (items ?? []) : filterHidden(items);\n}\n\n/**\n * English heading text for each section, keyed by {@link SectionId} (e.g.\n * `other-profiles` renders as \"Links\"). Exhaustive `Record<SectionId, string>`:\n * adding a section to {@link ALL_SECTIONS} without a label here is a compile\n * error, so a section can never silently render without (or with a stale)\n * heading. Consumers with i18n resolve their own localized labels via\n * `labelKey`; this is the canonical English fallback used by the exports.\n */\nexport const SECTION_LABELS: Record<SectionId, string> = {\n about: 'About',\n career: 'Career',\n skills: 'Skills',\n projects: 'Projects',\n presentations: 'Talks & sessions',\n publications: 'Publications',\n credentials: 'Credentials',\n education: 'Education',\n courses: 'Courses',\n awards: 'Awards',\n involvement: 'Involvement',\n investments: 'Investments',\n languages: 'Languages',\n 'other-profiles': 'Links',\n};\n","/** Shared URL helpers for the JSON-LD emitters. */\n\nexport const DEFAULT_BASE_URL = 'https://sifa.id';\n\n/**\n * Drop trailing slashes from a caller-supplied base URL.\n *\n * Written as a scan rather than `replace(/\\/+$/, '')` on purpose: the regex\n * form is a polynomial-backtracking hazard on input the library does not\n * control (CodeQL js/polynomial-redos). This runs in linear time.\n */\nexport function normaliseBaseUrl(baseUrl: string | undefined): string {\n const value = baseUrl ?? DEFAULT_BASE_URL;\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 0x2f) end -= 1;\n return value.slice(0, end);\n}\n\nexport function profileUrl(baseUrl: string, handle: string): string {\n return `${baseUrl}/p/${handle}`;\n}\n","/**\n * JSON Resume v1.0.0 emitter for a Sifa profile.\n *\n * https://jsonresume.org/schema/\n *\n * One pure function, no fetching, no DOM, no framework -- the same shape as the\n * JSON-LD emitter next door, and for the same reason: sifa.id, page.sifa.id and\n * any future consumer should emit one document from one implementation.\n *\n * Why this format earns its place: it is the lingua franca of CV tooling, so\n * emitting it hands a user every JSON Resume theme, and the Typst CV templates\n * on Typst Universe, without Sifa writing a renderer for any of them.\n *\n * Fields Sifa holds that JSON Resume has no slot for -- endorsements,\n * confirmations, involvement, investments, presentations, courses -- are\n * dropped rather than smuggled into a nearby field. The loss is documented for\n * the user at the point of download.\n */\n\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport type {\n ExternalAccount,\n LanguageProficiency,\n LocationValue,\n ProfileCertification,\n ProfileEducation,\n ProfileHonor,\n ProfileLanguage,\n ProfilePosition,\n ProfileProject,\n ProfilePublication,\n ProfileSkill,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normaliseBaseUrl, profileUrl } from '../jsonld/url.js';\nimport { normalizeExportContact, type ExportContact } from './export-contact.js';\n\nexport type Sanitizer = (input: string) => string;\n\nexport interface JsonResumeOptions {\n /** Canonical origin for the profile URL. Defaults to `https://sifa.id`. */\n readonly baseUrl?: string;\n /** Applied to every user-authored string. Defaults to identity. */\n readonly sanitize?: Sanitizer;\n /**\n * Absolute URL of the profile this resume describes. Defaults to\n * `${baseUrl}/p/${handle}`. A personal site served from a custom domain is\n * not at that path.\n */\n readonly canonicalUrl?: string;\n /**\n * Email, phone and postal address to print. Sifa holds none of these; the\n * owner types them on the export page. Without it the resume has no\n * `email`, `phone` or `location.address`.\n */\n readonly contact?: ExportContact;\n}\n\n/**\n * The subset of `Profile` this emitter reads. Deliberately structural rather\n * than the full `Profile`, so a caller holding a partially hydrated profile --\n * or a test -- does not have to invent forty unrelated fields.\n */\nexport interface JsonResumeProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly avatar?: string;\n readonly headline?: string;\n readonly about?: string;\n readonly website?: string;\n readonly location?: LocationValue | null;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly skills?: ProfileSkill[];\n readonly certifications?: ProfileCertification[];\n readonly projects?: ProfileProject[];\n readonly publications?: ProfilePublication[];\n readonly volunteering?: ProfileVolunteering[];\n readonly honors?: ProfileHonor[];\n readonly languages?: ProfileLanguage[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface JsonResumeLocation {\n /** Street address; lines are separated by `\\n`, as the schema asks. */\n readonly address?: string;\n readonly city?: string;\n readonly region?: string;\n readonly countryCode?: string;\n readonly postalCode?: string;\n}\n\nexport interface JsonResumeProfileLink {\n readonly network: string;\n readonly username?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumeBasics {\n readonly name: string;\n readonly label?: string;\n readonly image?: string;\n readonly url?: string;\n readonly email?: string;\n readonly phone?: string;\n readonly summary?: string;\n readonly location?: JsonResumeLocation;\n readonly profiles?: JsonResumeProfileLink[];\n}\n\nexport interface JsonResumeWork {\n readonly name?: string;\n readonly position: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeVolunteer {\n /**\n * Omitted when the record carries no organization name at all. Emitting an\n * empty string would render as a blank organization line in a CV template.\n */\n readonly organization?: string;\n readonly position?: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeEducation {\n /** Omitted when the record carries no institution name. See volunteer. */\n readonly institution?: string;\n readonly area?: string;\n readonly studyType?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeAward {\n readonly title: string;\n readonly date?: string;\n readonly awarder?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeCertificate {\n readonly name: string;\n readonly date?: string;\n readonly issuer?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumePublication {\n readonly name: string;\n readonly publisher?: string;\n readonly releaseDate?: string;\n readonly url?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeSkill {\n readonly name: string;\n}\n\nexport interface JsonResumeLanguage {\n readonly language: string;\n readonly fluency?: string;\n}\n\nexport interface JsonResumeProject {\n readonly name: string;\n readonly description?: string;\n readonly url?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeMeta {\n readonly canonical: string;\n readonly version: string;\n readonly lastModified?: string;\n}\n\nexport interface JsonResume {\n readonly basics?: JsonResumeBasics;\n readonly work?: JsonResumeWork[];\n readonly volunteer?: JsonResumeVolunteer[];\n readonly education?: JsonResumeEducation[];\n readonly awards?: JsonResumeAward[];\n readonly certificates?: JsonResumeCertificate[];\n readonly publications?: JsonResumePublication[];\n readonly skills?: JsonResumeSkill[];\n readonly languages?: JsonResumeLanguage[];\n readonly projects?: JsonResumeProject[];\n readonly meta?: JsonResumeMeta;\n}\n\n/** Schema version this emitter targets, as JSON Resume writes it in `meta`. */\nconst SCHEMA_VERSION = 'v1.0.0';\n\n/**\n * A date JSON Resume will accept: `YYYY`, `YYYY-MM` or `YYYY-MM-DD`. Sifa\n * stores RFC 3339 for some fields, and a full timestamp pasted into a CV\n * template renders as `2020-03-04T00:00:00.000Z` on the page.\n */\nconst RESUME_DATE_RE = /^(\\d{4})(-\\d{2})?(-\\d{2})?/;\n\nexport function toResumeDate(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n if (!trimmed) return undefined;\n const match = RESUME_DATE_RE.exec(trimmed);\n return match ? match[0] : undefined;\n}\n\n/**\n * JSON Resume's `fluency` is free text, so these are display labels rather\n * than a mapped vocabulary. Kept in one place so export and a future import\n * agree on the spelling.\n */\nconst FLUENCY_LABELS: Record<LanguageProficiency, string> = {\n elementary: 'Elementary',\n limited_working: 'Limited working',\n professional_working: 'Professional working',\n full_professional: 'Full professional',\n native: 'Native or bilingual',\n};\n\nconst identity: Sanitizer = (input) => input;\n\n/** Drops keys whose value is undefined, so the document carries no empty slots. */\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\n/** Adds the owner's street address to the profile's location. */\nfunction withAddress(\n location: JsonResumeLocation | undefined,\n address: string | undefined,\n): JsonResumeLocation | undefined {\n return address ? { address, ...location } : location;\n}\n\n/** Omits the key entirely when the mapped list came out empty. */\nfunction section<K extends string, T>(key: K, items: T[]): Record<K, T[]> | undefined {\n return items.length > 0 ? ({ [key]: items } as Record<K, T[]>) : undefined;\n}\n\nfunction mapLocation(\n location: LocationValue | null | undefined,\n s: Sanitizer,\n): JsonResumeLocation | undefined {\n if (!location) return undefined;\n const mapped = compact({\n city: text(location.locality ?? location.city, s),\n region: text(location.region, s),\n countryCode: text(location.countryCode, s),\n postalCode: text(location.postalCode, s),\n });\n return Object.keys(mapped).length > 0 ? mapped : undefined;\n}\n\n/** Trimmed, sanitised, and undefined when there is nothing left. */\nfunction text(value: string | undefined, s: Sanitizer): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? s(trimmed) : undefined;\n}\n\n/**\n * The name to print for a linked organization. `entityName` is the AppView's\n * resolved current name and wins when present, matching how the profile itself\n * renders these.\n */\nfunction orgName(\n entityName: string | undefined,\n stored: string | undefined,\n s: Sanitizer,\n): string | undefined {\n return text(entityName, s) ?? text(stored, s);\n}\n\n/**\n * Build a JSON Resume v1.0.0 document from a Sifa profile.\n *\n * Hidden records are dropped. Keys are omitted rather than emitted empty, so a\n * profile with no publications produces a document with no `publications` key\n * instead of one a reader has to interpret.\n */\nexport function profileToJsonResume(\n profile: JsonResumeProfileInput,\n options: JsonResumeOptions = {},\n): JsonResume {\n const s = options.sanitize ?? identity;\n const baseUrl = normaliseBaseUrl(options.baseUrl);\n const canonical = options.canonicalUrl ?? profileUrl(baseUrl, profile.handle);\n const contact = normalizeExportContact(options.contact ?? {});\n\n const name =\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName, s) ??\n profile.handle;\n\n const profiles = filterHidden(profile.externalAccounts).map((account) =>\n compact<JsonResumeProfileLink>({\n network: s(account.platform),\n username: text(account.label, s),\n url: text(account.url, s),\n }),\n );\n\n const basics = compact<JsonResumeBasics>({\n name,\n label: text(profile.headline, s),\n image: text(profile.avatar, s),\n email: text(contact.email, s),\n phone: text(contact.phone, s),\n url: text(profile.website, s) ?? canonical,\n summary: text(profile.about, s),\n location: withAddress(mapLocation(profile.location, s), text(contact.address, s)),\n ...section('profiles', profiles),\n });\n\n const work = filterHidden(profile.positions).map((position) =>\n compact<JsonResumeWork>({\n name: orgName(position.entityName, position.company, s),\n position: s(position.title),\n summary: text(position.description, s),\n startDate: toResumeDate(position.startedAt),\n endDate: toResumeDate(position.endedAt),\n }),\n );\n\n const volunteer = filterHidden(profile.volunteering).map((entry) =>\n compact<JsonResumeVolunteer>({\n organization: orgName(entry.entityName, entry.organization, s),\n position: text(entry.role, s),\n summary: text(entry.description, s),\n startDate: toResumeDate(entry.startDate),\n endDate: toResumeDate(entry.endDate),\n }),\n );\n\n const education = filterHidden(profile.education).map((entry) =>\n compact<JsonResumeEducation>({\n institution: orgName(entry.entityName, entry.institution, s),\n area: text(entry.fieldOfStudy, s),\n studyType: text(entry.degree, s),\n startDate: toResumeDate(entry.startedAt),\n endDate: toResumeDate(entry.endedAt),\n }),\n );\n\n const awards = filterHidden(profile.honors).map((honor) =>\n compact<JsonResumeAward>({\n title: s(honor.title),\n date: toResumeDate(honor.date),\n awarder: orgName(honor.entityName, honor.issuer, s),\n summary: text(honor.description, s),\n }),\n );\n\n const certificates = filterHidden(profile.certifications).map((cert) =>\n compact<JsonResumeCertificate>({\n name: s(cert.name),\n date: toResumeDate(cert.issueDate),\n // `issuingOrg` is the deprecated read-view alias for `authority`; records\n // indexed before the rename carry only the alias.\n issuer: orgName(cert.entityName, cert.authority ?? cert.issuingOrg, s),\n url: text(cert.credentialUrl, s),\n }),\n );\n\n const publications = filterHidden(profile.publications).map((publication) =>\n compact<JsonResumePublication>({\n name: s(publication.title),\n publisher: text(publication.publisher, s),\n releaseDate: toResumeDate(publication.date),\n url: text(publication.url, s),\n summary: text(publication.description, s),\n }),\n );\n\n // One entry per skill rather than JSON Resume's grouped shape: Sifa stores one\n // record per skill, and keeping the two symmetric is what makes a round-trip\n // through a third-party editor safe. Category and endorsement counts have no\n // JSON Resume slot and are dropped. Skill records carry no `hidden` flag, so\n // there is nothing to filter here.\n const skills = (profile.skills ?? []).map((skill) => ({ name: s(skill.name) }));\n\n const languages = filterHidden(profile.languages).map((language) =>\n compact<JsonResumeLanguage>({\n language: s(language.language),\n fluency: language.proficiency ? FLUENCY_LABELS[language.proficiency] : undefined,\n }),\n );\n\n const projects = filterHidden(profile.projects).map((project) =>\n compact<JsonResumeProject>({\n name: s(project.name),\n description: text(project.description, s),\n url: text(project.url, s),\n startDate: toResumeDate(project.startDate),\n endDate: toResumeDate(project.endDate),\n }),\n );\n\n return {\n basics,\n ...section('work', work),\n ...section('volunteer', volunteer),\n ...section('education', education),\n ...section('awards', awards),\n ...section('certificates', certificates),\n ...section('publications', publications),\n ...section('skills', skills),\n ...section('languages', languages),\n ...section('projects', projects),\n meta: { canonical, version: SCHEMA_VERSION },\n };\n}\n","import type { LocationValue } from '../types/index.js';\n\n/**\n * Format a LocationValue for display.\n *\n * Prefers `locality` (the community.lexicon.location.address field) and\n * falls back to the legacy `city` slot so values produced before the\n * sifa-api alias migration still render correctly during the additive\n * transition window.\n */\nexport function formatLocation(loc: LocationValue | null | undefined): string {\n if (!loc) return '';\n const locality = loc.locality ?? loc.city;\n const parts = [locality, loc.region, loc.country].filter(Boolean);\n if (loc.postalCode && !locality) {\n return `${loc.postalCode}, ${loc.country}`;\n }\n return parts.join(', ');\n}\n\n/** Parse a display string back into a LocationValue (best-effort for legacy data) */\nexport function parseLocationString(str: string): LocationValue | null {\n if (!str.trim()) return null;\n const parts = str\n .split(',')\n .map((p) => p.trim())\n .filter(Boolean);\n if (parts.length >= 3) {\n return {\n city: parts[0],\n region: parts[1],\n country: parts[parts.length - 1]!,\n };\n }\n if (parts.length === 2) {\n return {\n city: parts[0],\n country: parts[1]!,\n };\n }\n return { country: parts[0]! };\n}\n\n/** Convert ISO 3166-1 alpha-2 country code to flag emoji */\nexport function countryCodeToFlag(code: string | undefined): string {\n if (!code || code.length !== 2) return '';\n const upper = code.toUpperCase();\n const a = upper.codePointAt(0);\n const b = upper.codePointAt(1);\n if (a === undefined || b === undefined) return '';\n return String.fromCodePoint(a - 0x41 + 0x1f1e6, b - 0x41 + 0x1f1e6);\n}\n","/**\n * Course-role taxonomy. Mirrors `id.sifa.defs#courseRole.knownValues` from\n * sifa-lexicons. A course record without a role is a course the user took;\n * taught and assisted courses render under the \"Teaching\" heading.\n */\n\nexport interface CourseRoleOption {\n value: string;\n label: string;\n}\n\nexport const COURSE_ROLE_TAKEN = 'id.sifa.defs#courseTaken';\nexport const COURSE_ROLE_TAUGHT = 'id.sifa.defs#courseTaught';\nexport const COURSE_ROLE_TEACHING_ASSISTANT = 'id.sifa.defs#courseTeachingAssistant';\n\nexport const COURSE_ROLE_OPTIONS: CourseRoleOption[] = [\n { value: COURSE_ROLE_TAKEN, label: 'Student' },\n { value: COURSE_ROLE_TAUGHT, label: 'Instructor' },\n { value: COURSE_ROLE_TEACHING_ASSISTANT, label: 'Teaching assistant' },\n];\n\nexport const COURSE_ROLE_LABELS: Record<string, string> = Object.fromEntries(\n COURSE_ROLE_OPTIONS.map((o) => [o.value, o.label]),\n);\n\n/**\n * Resolve the label for a course-role token. An unknown token resolves to\n * `undefined` rather than the raw value, so a token a client has not learned\n * never shows up as a raw `id.sifa.defs#...` string.\n */\nexport function getCourseRoleLabel(value: string | undefined | null): string | undefined {\n if (!value) return undefined;\n return COURSE_ROLE_LABELS[value];\n}\n\nconst TEACHING_ROLES = new Set([COURSE_ROLE_TAUGHT, COURSE_ROLE_TEACHING_ASSISTANT]);\n\n/**\n * Whether a course is one the user taught or assisted with. An absent, taken,\n * or unknown role counts as taken, so a record is never dropped from the\n * profile when a client meets a token it has not learned.\n */\nexport function isTeachingCourse(course: { role?: string | null }): boolean {\n return !!course.role && TEACHING_ROLES.has(course.role);\n}\n\n/** Split courses into the ones taken and the ones taught or assisted, keeping order. */\nexport function splitCoursesByRole<T extends { role?: string | null }>(\n courses: readonly T[],\n): { taken: T[]; teaching: T[] } {\n const taken: T[] = [];\n const teaching: T[] = [];\n for (const c of courses) (isTeachingCourse(c) ? teaching : taken).push(c);\n return { taken, teaching };\n}\n","/**\n * Funder-format CV documents: the NIH Biographical Sketch (Common Form plus\n * NIH Supplement) and the ERC Part B1 \"Curriculum vitae and Track Record\".\n *\n * A pure mapper from a Sifa profile and the owner's pick of items to a plain\n * JSON document a typesetter lays out. Like the JSON Resume emitter next door,\n * nothing here fetches or renders.\n *\n * Funders cap how many items a CV may list. The caps live in\n * `FUNDER_CV_GROUPS`, so the export page that offers the choice and the server\n * that enforces it read one number. Sections a funder asks for that Sifa holds\n * no records for (a personal statement, contributions to science, career\n * breaks) are emitted as `null` or empty, for the template to show as a\n * placeholder the applicant fills in.\n */\n\nimport { z } from 'zod';\n\nimport { formatLocation } from '../format/location-utils.js';\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport { isTeachingCourse } from '../taxonomy/course-role.js';\nimport type {\n ExternalAccount,\n ProfileCourse,\n ProfileEducation,\n ProfileHonor,\n ProfilePosition,\n ProfilePresentation,\n ProfilePublication,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normalizeExportContact, type ExportContact } from './export-contact.js';\nimport { toResumeDate } from './json-resume.js';\n\nexport const FUNDER_CV_FORMATS = ['nih', 'erc'] as const;\nexport type FunderCvFormat = (typeof FUNDER_CV_FORMATS)[number];\n\nexport const FUNDER_CV_ITEM_KINDS = ['publication', 'presentation', 'honor'] as const;\nexport type FunderCvItemKind = (typeof FUNDER_CV_ITEM_KINDS)[number];\n\nexport interface FunderCvGroupSpec {\n readonly kinds: readonly FunderCvItemKind[];\n /** Most items the funder's form takes for this group. */\n readonly max: number;\n}\n\n/**\n * The pickable groups per format, with the funder's item limits.\n *\n * - NIH Common Form \"Products\": up to 5 closely related to the proposed\n * project and up to 5 other significant products. Publications, conference\n * papers and presentations all count. NIH Supplement \"Honors\": up to 15.\n * - ERC Part B1 \"Research achievements\": up to ten research outputs. \"Peer\n * recognition\" is \"selected examples\" with no number; 15 is Sifa's own cap,\n * to keep the section inside the 4-page limit.\n */\nexport const FUNDER_CV_GROUPS = {\n nih: {\n related: { kinds: ['publication', 'presentation'], max: 5 },\n other: { kinds: ['publication', 'presentation'], max: 5 },\n honors: { kinds: ['honor'], max: 15 },\n },\n erc: {\n outputs: { kinds: ['publication'], max: 10 },\n recognition: { kinds: ['honor', 'presentation'], max: 15 },\n },\n} as const satisfies Record<FunderCvFormat, Record<string, FunderCvGroupSpec>>;\n\nexport type FunderCvGroupName<F extends FunderCvFormat> = keyof (typeof FUNDER_CV_GROUPS)[F] &\n string;\n\n/** A picked item: `<kind>:<rkey>`, e.g. `publication:3kxyz`. */\nexport function funderCvItemId(kind: FunderCvItemKind, rkey: string): string {\n return `${kind}:${rkey}`;\n}\n\n/** Record key syntax (atproto): 1-512 of A-Z a-z 0-9 . - _ : ~ */\nconst ITEM_ID_RE = /^(publication|presentation|honor):[A-Za-z0-9._~:-]{1,512}$/;\n\nfunction kindOf(id: string): FunderCvItemKind | undefined {\n const kind = id.slice(0, id.indexOf(':'));\n return (FUNDER_CV_ITEM_KINDS as readonly string[]).includes(kind)\n ? (kind as FunderCvItemKind)\n : undefined;\n}\n\nfunction groupSchema(spec: FunderCvGroupSpec) {\n return z\n .array(\n z\n .string()\n .regex(ITEM_ID_RE)\n .refine((id) => spec.kinds.includes(kindOf(id) as FunderCvItemKind), {\n message: `Only ${spec.kinds.join(', ')} items fit here`,\n }),\n )\n .max(spec.max)\n .default([]);\n}\n\nfunction noRepeats(lists: readonly string[][]): boolean {\n const all = lists.flat();\n return new Set(all).size === all.length;\n}\n\nconst nihSelectionSchema = z\n .object({\n format: z.literal('nih'),\n related: groupSchema(FUNDER_CV_GROUPS.nih.related),\n other: groupSchema(FUNDER_CV_GROUPS.nih.other),\n honors: groupSchema(FUNDER_CV_GROUPS.nih.honors),\n })\n .refine((s) => noRepeats([s.related, s.other, s.honors]), {\n message: 'An item can be listed once',\n });\n\nconst ercSelectionSchema = z\n .object({\n format: z.literal('erc'),\n outputs: groupSchema(FUNDER_CV_GROUPS.erc.outputs),\n recognition: groupSchema(FUNDER_CV_GROUPS.erc.recognition),\n })\n .refine((s) => noRepeats([s.outputs, s.recognition]), {\n message: 'An item can be listed once',\n });\n\n/**\n * The owner's pick for one format. Enforces the shape and the funder's limits;\n * whether each id is one of the owner's own visible records is a separate\n * check against the profile, `findUnknownFunderCvItems`.\n */\nexport const funderCvSelectionSchema = z.discriminatedUnion('format', [\n nihSelectionSchema,\n ercSelectionSchema,\n]);\n\nexport type FunderCvSelection = z.infer<typeof funderCvSelectionSchema>;\n\n/** The subset of `Profile` the funder mapper reads. */\nexport interface FunderCvProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly headline?: string;\n readonly website?: string;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly publications?: ProfilePublication[];\n readonly presentations?: ProfilePresentation[];\n readonly honors?: ProfileHonor[];\n readonly courses?: ProfileCourse[];\n readonly volunteering?: ProfileVolunteering[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface FunderCvCandidate {\n readonly id: string;\n readonly kind: FunderCvItemKind;\n readonly title: string;\n /** `YYYY`, `YYYY-MM` or `YYYY-MM-DD`; absent when the record has no date. */\n readonly date?: string;\n}\n\n/** The most recent visible delivery of a talk, which dates it. */\nfunction latestDeliveryRecord(presentation: ProfilePresentation) {\n const dateOf = (d: { date?: string | null }) => toResumeDate(d.date ?? undefined) ?? '';\n return [...filterHidden(presentation.deliveries)]\n .sort((a, b) => dateOf(a).localeCompare(dateOf(b)))\n .at(-1);\n}\n\nfunction latestDelivery(presentation: ProfilePresentation): string | undefined {\n return toResumeDate(latestDeliveryRecord(presentation)?.date ?? undefined);\n}\n\nfunction candidatesOf(profile: FunderCvProfileInput, kind: FunderCvItemKind): FunderCvCandidate[] {\n switch (kind) {\n case 'publication':\n return filterHidden(profile.publications).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: toResumeDate(p.date),\n }));\n case 'presentation':\n return filterHidden(profile.presentations).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: latestDelivery(p),\n }));\n case 'honor':\n return filterHidden(profile.honors).map((h) => ({\n id: funderCvItemId(kind, h.rkey),\n kind,\n title: h.title,\n date: toResumeDate(h.date),\n }));\n }\n}\n\n/**\n * The owner's visible items of the given kinds, most recent first. Undated\n * items go last, in profile order. The sort is stable, so equal dates keep\n * profile order too.\n */\nexport function listFunderCvCandidates(\n profile: FunderCvProfileInput,\n kinds: readonly FunderCvItemKind[],\n): FunderCvCandidate[] {\n const all = kinds.flatMap((kind) => candidatesOf(profile, kind));\n return all.sort((a, b) => {\n if (a.date && b.date) return b.date.localeCompare(a.date);\n if (a.date) return -1;\n if (b.date) return 1;\n return 0;\n });\n}\n\n/**\n * What the export page pre-ticks: the most recent publications up to each\n * limit (for NIH, the first five as closely related and the next five as\n * other), and the most recent honors. Talks are offered but not pre-ticked.\n */\nexport function defaultFunderCvSelection(\n profile: FunderCvProfileInput,\n format: FunderCvFormat,\n): FunderCvSelection {\n const publications = listFunderCvCandidates(profile, ['publication']).map((c) => c.id);\n const honors = listFunderCvCandidates(profile, ['honor']).map((c) => c.id);\n if (format === 'nih') {\n const { related, other } = FUNDER_CV_GROUPS.nih;\n return {\n format,\n related: publications.slice(0, related.max),\n other: publications.slice(related.max, related.max + other.max),\n honors: honors.slice(0, FUNDER_CV_GROUPS.nih.honors.max),\n };\n }\n return {\n format,\n outputs: publications.slice(0, FUNDER_CV_GROUPS.erc.outputs.max),\n recognition: honors.slice(0, FUNDER_CV_GROUPS.erc.recognition.max),\n };\n}\n\nfunction selectedIds(selection: FunderCvSelection): string[] {\n return selection.format === 'nih'\n ? [...selection.related, ...selection.other, ...selection.honors]\n : [...selection.outputs, ...selection.recognition];\n}\n\n/**\n * Ids in the selection that are not visible records on this profile. The\n * profile is the owner's own, so an empty result means every pick is theirs.\n */\nexport function findUnknownFunderCvItems(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n): string[] {\n const known = new Set(listFunderCvCandidates(profile, FUNDER_CV_ITEM_KINDS).map((c) => c.id));\n return selectedIds(selection).filter((id) => !known.has(id));\n}\n\n// ---------------------------------------------------------------------------\n// Document shapes\n// ---------------------------------------------------------------------------\n\nexport interface FunderCvPerson {\n /** Full name as the profile shows it; the handle when there is no name. */\n readonly name: string;\n readonly givenName?: string;\n readonly familyName?: string;\n /** Bare ORCID iD, e.g. `0000-0002-1825-0097`. */\n readonly orcid?: string;\n readonly website?: string;\n}\n\n/**\n * The ERC person, with the contact details the owner typed for this export.\n * The NIH Biographical Sketch has no contact block, so only ERC carries them.\n */\nexport interface ErcFunderCvPerson extends FunderCvPerson {\n readonly email?: string;\n readonly phone?: string;\n /** Lines are separated by `\\n`. */\n readonly address?: string;\n}\n\nexport interface FunderCvOptions {\n /**\n * Contact details the owner typed for this export. Sifa stores none of\n * them. Used for ERC only.\n */\n readonly contact?: ExportContact;\n}\n\n/** One product or research output, ready to cite. */\nexport interface FunderCvCitation {\n readonly kind: 'publication' | 'presentation';\n readonly title: string;\n /** Authors in record order, comma separated. For a talk, the speaker. */\n readonly authors?: string;\n /** Journal, publisher or event. */\n readonly venue?: string;\n /** Year. */\n readonly date?: string;\n readonly doi?: string;\n readonly url?: string;\n}\n\nexport interface FunderCvHonor {\n readonly year?: string;\n readonly title: string;\n readonly organization?: string;\n}\n\nexport interface NihEducation {\n readonly organization: string;\n readonly location?: string;\n readonly degree?: string;\n /** Start date, `MM/YYYY` or `YYYY`. */\n readonly start?: string;\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly end?: string;\n readonly field?: string;\n}\n\nexport interface FunderCvAppointment {\n /** `YYYY`. */\n readonly start?: string;\n /** `YYYY`, or null for a current appointment. */\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n readonly location?: string;\n}\n\n/** NIH Biographical Sketch: Common Form plus NIH Supplement. */\nexport interface NihFunderCv {\n readonly format: 'nih';\n readonly person: FunderCvPerson;\n readonly positionTitle?: string;\n /** Primary organization: the current position's. */\n readonly organization?: string;\n readonly location?: string;\n readonly education: NihEducation[];\n readonly appointments: FunderCvAppointment[];\n readonly products: { readonly related: FunderCvCitation[]; readonly other: FunderCvCitation[] };\n readonly honors: FunderCvHonor[];\n /** Supplement \"Personal Statement\". Sifa holds no record for it. */\n readonly personalStatement: string | null;\n /** Supplement \"Contributions to Science\". Sifa holds no record for them. */\n readonly contributions: string[];\n}\n\nexport interface ErcEducation {\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly date?: string;\n readonly degree?: string;\n readonly field?: string;\n readonly institution: string;\n}\n\nexport interface ErcRecognition {\n readonly kind: 'honor' | 'presentation';\n readonly year?: string;\n readonly title: string;\n /** Awarding body, or the event a talk was given at. */\n readonly organization?: string;\n}\n\nexport interface ErcContribution {\n readonly kind: 'teaching' | 'service';\n readonly start?: string;\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n}\n\n/** ERC Part B1, section \"Curriculum vitae and Track Record\". */\nexport interface ErcFunderCv {\n readonly format: 'erc';\n readonly person: ErcFunderCvPerson;\n readonly education: ErcEducation[];\n readonly currentPositions: FunderCvAppointment[];\n readonly previousPositions: FunderCvAppointment[];\n readonly outputs: FunderCvCitation[];\n readonly recognition: ErcRecognition[];\n readonly communityContributions: ErcContribution[];\n /** Proposal acronym for the page header. Not a profile fact. */\n readonly acronym: string | null;\n /** \"Career breaks, diverse career paths and major life events\". */\n readonly careerBreaks: string | null;\n}\n\nexport type FunderCvDocument = NihFunderCv | ErcFunderCv;\n\n// ---------------------------------------------------------------------------\n// Mapping\n// ---------------------------------------------------------------------------\n\nfunction text(value: string | null | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\nfunction year(value: string | null | undefined): string | undefined {\n return toResumeDate(value ?? undefined)?.slice(0, 4);\n}\n\n/** `MM/YYYY` when the month is known, else `YYYY`. */\nfunction monthYear(value: string | null | undefined): string | undefined {\n const date = toResumeDate(value ?? undefined);\n if (!date) return undefined;\n return date.length >= 7 ? `${date.slice(5, 7)}/${date.slice(0, 4)}` : date;\n}\n\n/** Newest start first; undated entries last, in profile order. */\nfunction byStartDesc<T>(items: T[], start: (item: T) => string | undefined): T[] {\n return [...items].sort((a, b) => {\n const da = toResumeDate(start(a));\n const db = toResumeDate(start(b));\n if (da && db) return db.localeCompare(da);\n if (da) return -1;\n if (db) return 1;\n return 0;\n });\n}\n\nconst ORCID_RE = /\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX]/;\n\nfunction orcidOf(accounts: ExternalAccount[] | undefined): string | undefined {\n const account = filterHidden(accounts).find((a) => a.platform.toLowerCase() === 'orcid');\n return account ? ORCID_RE.exec(account.url)?.[0] : undefined;\n}\n\nfunction personOf(profile: FunderCvProfileInput): FunderCvPerson {\n return compact<FunderCvPerson>({\n name:\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName) ??\n profile.handle,\n givenName: text(profile.givenName),\n familyName: text(profile.familyName),\n orcid: orcidOf(profile.externalAccounts),\n website: text(profile.website),\n });\n}\n\nfunction appointment(position: ProfilePosition): FunderCvAppointment {\n return compact<FunderCvAppointment>({\n start: year(position.startedAt),\n end: year(position.endedAt) ?? null,\n title: position.title,\n organization: text(position.entityName) ?? text(position.company),\n location: text(formatLocation(position.location)),\n });\n}\n\n/** The current position to name on the form: the flagged primary, else the newest. */\nfunction currentPosition(positions: ProfilePosition[]): ProfilePosition | undefined {\n const current = byStartDesc(\n positions.filter((p) => !text(p.endedAt)),\n (p) => p.startedAt,\n );\n return current.find((p) => p.primary) ?? current[0];\n}\n\nfunction lookup(profile: FunderCvProfileInput) {\n const publications = new Map(filterHidden(profile.publications).map((p) => [p.rkey, p]));\n const presentations = new Map(filterHidden(profile.presentations).map((p) => [p.rkey, p]));\n const honors = new Map(filterHidden(profile.honors).map((h) => [h.rkey, h]));\n const split = (id: string) => {\n const at = id.indexOf(':');\n return { kind: id.slice(0, at), rkey: id.slice(at + 1) };\n };\n return { publications, presentations, honors, split };\n}\n\nfunction citations(\n ids: readonly string[],\n profile: FunderCvProfileInput,\n person: FunderCvPerson,\n): FunderCvCitation[] {\n const { publications, presentations, split } = lookup(profile);\n return ids.flatMap((id): FunderCvCitation[] => {\n const { kind, rkey } = split(id);\n if (kind === 'publication') {\n const p = publications.get(rkey);\n if (!p) return [];\n const authors = (p.contributors ?? []).map((c) => c.name.trim()).filter(Boolean);\n return [\n compact<FunderCvCitation>({\n kind: 'publication',\n title: p.title,\n authors: authors.length > 0 ? authors.join(', ') : undefined,\n venue: text(p.publisher),\n date: year(p.date),\n doi: text(p.doi),\n url: text(p.url),\n }),\n ];\n }\n if (kind === 'presentation') {\n const talk = presentations.get(rkey);\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<FunderCvCitation>({\n kind: 'presentation',\n title: talk.title,\n authors: person.name,\n venue: text(delivery?.eventName),\n date: year(delivery?.date),\n url: text(talk.links?.[0]?.uri),\n }),\n ];\n }\n return [];\n });\n}\n\nfunction honorsOf(ids: readonly string[], profile: FunderCvProfileInput): FunderCvHonor[] {\n const { honors, split } = lookup(profile);\n return ids.flatMap((id) => {\n const { kind, rkey } = split(id);\n const h = kind === 'honor' ? honors.get(rkey) : undefined;\n if (!h) return [];\n return [\n compact<FunderCvHonor>({\n year: year(h.date),\n title: h.title,\n organization: text(h.entityName) ?? text(h.issuer),\n }),\n ];\n });\n}\n\n/**\n * Build the funder document from the profile and the owner's selection.\n *\n * Hidden records never appear. Selected ids that match no visible record are\n * skipped; callers that must refuse them check `findUnknownFunderCvItems`\n * first. Selected items keep the order they were picked in.\n */\nexport function profileToFunderCv(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n options: FunderCvOptions = {},\n): FunderCvDocument {\n const person = personOf(profile);\n const positions = filterHidden(profile.positions);\n const education = byStartDesc(filterHidden(profile.education), (e) => e.startedAt);\n\n if (selection.format === 'nih') {\n const current = currentPosition(positions);\n return compact<NihFunderCv>({\n format: 'nih',\n person,\n positionTitle: text(current?.title),\n organization: current ? appointment(current).organization : undefined,\n location: text(formatLocation(current?.location)),\n education: education.map((e) =>\n compact<NihEducation>({\n organization: text(e.entityName) ?? e.institution,\n degree: text(e.degree),\n start: monthYear(e.startedAt),\n end: monthYear(e.endedAt),\n field: text(e.fieldOfStudy),\n }),\n ),\n appointments: byStartDesc(positions, (p) => p.startedAt).map(appointment),\n products: {\n related: citations(selection.related, profile, person),\n other: citations(selection.other, profile, person),\n },\n honors: honorsOf(selection.honors, profile),\n personalStatement: null,\n contributions: [],\n });\n }\n\n const appointments = byStartDesc(positions, (p) => p.startedAt);\n const { presentations, split } = lookup(profile);\n\n const recognition = selection.recognition.flatMap((id): ErcRecognition[] => {\n const { kind, rkey } = split(id);\n if (kind === 'honor') {\n const [honor] = honorsOf([id], profile);\n return honor ? [{ kind: 'honor', ...honor }] : [];\n }\n const talk = kind === 'presentation' ? presentations.get(rkey) : undefined;\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<ErcRecognition>({\n kind: 'presentation',\n year: year(delivery?.date),\n title: talk.title,\n organization: text(delivery?.eventName),\n }),\n ];\n });\n\n const teaching = filterHidden(profile.courses)\n .filter(isTeachingCourse)\n .map((c) =>\n compact<ErcContribution>({\n kind: 'teaching',\n start: year(c.startedAt),\n end: year(c.endedAt) ?? null,\n title: c.name,\n organization: text(c.entityName) ?? text(c.institution),\n }),\n );\n const service = filterHidden(profile.volunteering).map((v) =>\n compact<ErcContribution>({\n kind: 'service',\n start: year(v.startDate),\n end: year(v.endDate) ?? null,\n title: text(v.role) ?? text(v.entityName) ?? v.organization,\n organization: text(v.entityName) ?? text(v.organization),\n }),\n );\n\n return {\n format: 'erc',\n person: { ...person, ...normalizeExportContact(options.contact ?? {}) },\n education: education.map((e) =>\n compact<ErcEducation>({\n date: monthYear(e.endedAt) ?? monthYear(e.startedAt),\n degree: text(e.degree),\n field: text(e.fieldOfStudy),\n institution: text(e.entityName) ?? e.institution,\n }),\n ),\n currentPositions: appointments.filter((p) => !text(p.endedAt)).map(appointment),\n previousPositions: appointments.filter((p) => !!text(p.endedAt)).map(appointment),\n outputs: citations(selection.outputs, profile, person),\n recognition,\n communityContributions: [...teaching, ...service],\n acronym: null,\n careerBreaks: null,\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/resume/export-contact.ts","../../src/format/pds-utils.ts","../../src/profile/section-model.ts","../../src/jsonld/url.ts","../../src/resume/json-resume.ts","../../src/format/location-utils.ts","../../src/taxonomy/course-role.ts","../../src/resume/funder-cv.ts"],"names":["z","text","compact"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * Private contact and personal details for exports: email, phone, postal\n * address, driver's license, right to work and nationality.\n *\n * Sifa never stores these and never writes them to the owner's PDS, where they\n * would be public. The owner types them on the export page, the browser keeps\n * them, and they go only into the files the owner downloads. This module holds\n * the shape, the limits and the pick rule, so the export page and the server\n * that builds a funder CV agree on them.\n */\n\nimport { z } from 'zod';\n\n/** Longest value accepted per field. 254 is the longest valid email address. */\nexport const EXPORT_CONTACT_LIMITS = {\n email: 254,\n phone: 40,\n address: 500,\n driversLicense: 60,\n rightToWork: 200,\n nationality: 100,\n} as const;\n\n/** The contact details that go into an export. Absent keys are left out. */\nexport interface ExportContact {\n readonly email?: string;\n readonly phone?: string;\n /** Free text; lines are separated by `\\n`. */\n readonly address?: string;\n /** License categories, e.g. \"B, BE\". */\n readonly driversLicense?: string;\n /** e.g. \"EU citizen, no sponsorship needed\". */\n readonly rightToWork?: string;\n readonly nationality?: string;\n}\n\n/** What the owner has typed, plus which fields they chose to include. */\nexport interface ExportContactDraft {\n readonly email?: string;\n readonly phone?: string;\n readonly address?: string;\n readonly driversLicense?: string;\n readonly rightToWork?: string;\n readonly nationality?: string;\n readonly includeEmail?: boolean;\n readonly includePhone?: boolean;\n readonly includeAddress?: boolean;\n readonly includeDriversLicense?: boolean;\n readonly includeRightToWork?: boolean;\n readonly includeNationality?: boolean;\n}\n\n/**\n * Validates a picked contact. Only lengths are checked: phone numbers and\n * addresses vary too much between countries, and a wrong-looking email is a\n * warning for the owner, not a reason to refuse the export.\n */\nexport const exportContactSchema = z.strictObject({\n email: z.string().max(EXPORT_CONTACT_LIMITS.email).optional(),\n phone: z.string().max(EXPORT_CONTACT_LIMITS.phone).optional(),\n address: z.string().max(EXPORT_CONTACT_LIMITS.address).optional(),\n driversLicense: z.string().max(EXPORT_CONTACT_LIMITS.driversLicense).optional(),\n rightToWork: z.string().max(EXPORT_CONTACT_LIMITS.rightToWork).optional(),\n nationality: z.string().max(EXPORT_CONTACT_LIMITS.nationality).optional(),\n});\n\n/**\n * The fields to put in an export: the included ones that are not blank, run\n * through {@link normalizeExportContact}.\n */\nexport function pickExportContact(draft: ExportContactDraft): ExportContact {\n return normalizeExportContact({\n email: draft.includeEmail ? draft.email : undefined,\n phone: draft.includePhone ? draft.phone : undefined,\n address: draft.includeAddress ? draft.address : undefined,\n driversLicense: draft.includeDriversLicense ? draft.driversLicense : undefined,\n rightToWork: draft.includeRightToWork ? draft.rightToWork : undefined,\n nationality: draft.includeNationality ? draft.nationality : undefined,\n });\n}\n\n/**\n * Drops blank fields and tidies the rest. Every field but the address becomes\n * one line with single spaces. An address keeps its line breaks but loses blank lines and the\n * spaces around each line. Each field is cut at its {@link EXPORT_CONTACT_LIMITS}\n * length. The emitters call this too, so a caller that skips\n * {@link pickExportContact} and the schema gets the same bounded output.\n */\nexport function normalizeExportContact(contact: ExportContact): ExportContact {\n const email = cap(oneLine(contact.email), EXPORT_CONTACT_LIMITS.email);\n const phone = cap(oneLine(contact.phone), EXPORT_CONTACT_LIMITS.phone);\n const address = cap(lines(contact.address), EXPORT_CONTACT_LIMITS.address);\n const driversLicense = cap(oneLine(contact.driversLicense), EXPORT_CONTACT_LIMITS.driversLicense);\n const rightToWork = cap(oneLine(contact.rightToWork), EXPORT_CONTACT_LIMITS.rightToWork);\n const nationality = cap(oneLine(contact.nationality), EXPORT_CONTACT_LIMITS.nationality);\n return {\n ...(email ? { email } : {}),\n ...(phone ? { phone } : {}),\n ...(address ? { address } : {}),\n ...(driversLicense ? { driversLicense } : {}),\n ...(rightToWork ? { rightToWork } : {}),\n ...(nationality ? { nationality } : {}),\n };\n}\n\n/** A loose shape check for a non-blocking \"check this address\" hint. */\nexport function looksLikeEmail(value: string): boolean {\n const trimmed = value.trim();\n if (/\\s/.test(trimmed)) return false;\n const parts = trimmed.split('@');\n if (parts.length !== 2) return false;\n const [local, domain] = parts as [string, string];\n const dot = domain.lastIndexOf('.');\n return local.length > 0 && dot > 0 && dot < domain.length - 1;\n}\n\nfunction cap(value: string | undefined, max: number): string | undefined {\n if (value === undefined) return undefined;\n let cut = value.slice(0, max);\n // Never leave half of a surrogate pair (an emoji, say) at the cut.\n if (/[\\uD800-\\uDBFF]$/.test(cut)) cut = cut.slice(0, -1);\n return cut.trimEnd();\n}\n\nfunction oneLine(value: string | undefined): string | undefined {\n const collapsed = value?.replace(/\\s+/g, ' ').trim();\n return collapsed ? collapsed : undefined;\n}\n\nfunction lines(value: string | undefined): string | undefined {\n const kept = (value ?? '')\n .split(/\\r?\\n/)\n .map((line) => line.trim())\n .filter(Boolean);\n return kept.length > 0 ? kept.join('\\n') : undefined;\n}\n","import type { PdsProviderInfo } from '../types/index.js';\n\nexport interface PdsProvider {\n name: string;\n profileUrl: string;\n host?: string;\n}\n\nconst BSKY_PROFILE_BASE = 'https://bsky.app/profile/';\n\nconst PDS_PROVIDERS: { suffix: string; name: string }[] = [\n { suffix: '.bsky.social', name: 'bluesky' },\n { suffix: '.blacksky.app', name: 'blacksky' },\n { suffix: '.eurosky.social', name: 'eurosky' },\n { suffix: '.northsky.social', name: 'northsky' },\n];\n\nconst KNOWN_PROVIDER_NAMES = new Set(PDS_PROVIDERS.map((p) => p.name));\n\nconst ICON_ONLY_PROVIDERS = new Set(['selfhosted-social', 'selfhosted']);\n\nexport function pdsProviderFromApi(\n apiProvider: PdsProviderInfo | null | undefined,\n handle: string,\n): PdsProvider | null {\n if (!apiProvider) return null;\n if (ICON_ONLY_PROVIDERS.has(apiProvider.name)) {\n return { name: apiProvider.name, profileUrl: '', host: apiProvider.host };\n }\n if (!KNOWN_PROVIDER_NAMES.has(apiProvider.name)) return null;\n return {\n name: apiProvider.name,\n profileUrl: `${BSKY_PROFILE_BASE}${handle}`,\n host: apiProvider.host,\n };\n}\n\nexport function getHandleStem(handle: string): string {\n const lower = handle.toLowerCase();\n for (const provider of PDS_PROVIDERS) {\n if (lower.endsWith(provider.suffix) && lower.length > provider.suffix.length) {\n return handle.slice(0, -provider.suffix.length);\n }\n }\n return handle;\n}\n\nexport function getDisplayLabel(displayName: string | undefined, handle: string): string {\n if (displayName) return displayName;\n return getHandleStem(handle);\n}\n\n/**\n * Combine optional structured name fields (`id.sifa.profile.self.givenName`,\n * `familyName`) into a single string in Schema.org `Person` order — given\n * before family. Returns `undefined` when both are absent so callers can\n * fall back to `displayName`. Whitespace-only inputs are treated as absent.\n * Does not impose cultural ordering: users whose preferred presentation\n * differs (e.g. Eastern name order) should set `displayName` accordingly,\n * which this helper deliberately does not consult.\n */\nexport function formatStructuredName(\n givenName: string | undefined,\n familyName: string | undefined,\n): string | undefined {\n const given = givenName?.trim();\n const family = familyName?.trim();\n if (given && family) return `${given} ${family}`;\n return given || family || undefined;\n}\n\nconst PDS_DISPLAY_NAMES: Record<string, string> = {\n bluesky: 'Bluesky',\n blacksky: 'BlackSky',\n eurosky: 'EuroSky',\n northsky: 'NorthSky',\n 'selfhosted-social': 'Self-hosted',\n selfhosted: 'Self-hosted',\n};\n\nexport function getPdsDisplayName(providerName: string): string {\n return PDS_DISPLAY_NAMES[providerName] ?? providerName;\n}\n\nexport function detectPdsProvider(handle: string): PdsProvider | null {\n const lower = handle.toLowerCase();\n for (const provider of PDS_PROVIDERS) {\n if (lower.endsWith(provider.suffix)) {\n return {\n name: provider.name,\n profileUrl: `${BSKY_PROFILE_BASE}${handle}`,\n };\n }\n }\n return null;\n}\n","/**\n * Shared, single-source-of-truth model for which profile sections render, in\n * what order, their nav grouping, and which items a given viewer may see.\n *\n * Every Sifa profile surface builds on this: the HTML profile page, the\n * Markdown / DOCX / print exports, and the standalone personal-site renderer.\n * Adding a section to {@link ALL_SECTIONS} makes it appear everywhere (the\n * exports drive presence + order from here, and {@link SECTION_LABELS} is an\n * exhaustive `Record<SectionId, string>`, so a new id is a compile error until\n * it has a label).\n *\n * The per-section sorts live in `./section-sorts.js`, built on the same SDK\n * date extractors the HTML section components use, so the surfaces cannot\n * diverge on ordering.\n */\n\nimport type { Profile } from '../types/index.js';\n\n/**\n * Side-nav groups, in display order. Each labels a contiguous run of sections\n * in {@link ALL_SECTIONS}. `labelKey` resolves against the consumer's `sections`\n * i18n namespace.\n */\nexport const SECTION_GROUPS = [\n { id: 'overview', labelKey: 'groupOverview' },\n { id: 'experience', labelKey: 'groupExperience' },\n { id: 'qualifications', labelKey: 'groupQualifications' },\n { id: 'more', labelKey: 'groupMore' },\n] as const;\n\nexport type SectionGroupId = (typeof SECTION_GROUPS)[number]['id'];\n\n/**\n * Profile body sections in render + nav order (evidence-first): identity and\n * activity lead, then living work (career, projects, talks, publications, then\n * skills), then the formal record (credentials, education, courses), then the\n * rest. `ns` selects the consumer's i18n namespace for `labelKey`; `group`\n * ties the section to a {@link SECTION_GROUPS} header.\n */\nexport const ALL_SECTIONS = [\n { id: 'about', labelKey: 'about', ns: 'profile' as const, group: 'overview' as const },\n { id: 'career', labelKey: 'career', ns: 'sections' as const, group: 'experience' as const },\n { id: 'projects', labelKey: 'projects', ns: 'sections' as const, group: 'experience' as const },\n {\n id: 'presentations',\n labelKey: 'talksAndSessions',\n ns: 'sections' as const,\n group: 'experience' as const,\n },\n {\n id: 'publications',\n labelKey: 'publications',\n ns: 'sections' as const,\n group: 'experience' as const,\n },\n { id: 'skills', labelKey: 'skills', ns: 'sections' as const, group: 'experience' as const },\n {\n id: 'credentials',\n labelKey: 'credentials',\n ns: 'sections' as const,\n group: 'qualifications' as const,\n },\n {\n id: 'education',\n labelKey: 'education',\n ns: 'sections' as const,\n group: 'qualifications' as const,\n },\n { id: 'courses', labelKey: 'courses', ns: 'sections' as const, group: 'qualifications' as const },\n { id: 'awards', labelKey: 'awards', ns: 'sections' as const, group: 'more' as const },\n { id: 'involvement', labelKey: 'involvement', ns: 'sections' as const, group: 'more' as const },\n // Its own section rather than a group under involvement: that record is about\n // work contributed, this one about capital deployed.\n { id: 'investments', labelKey: 'investments', ns: 'sections' as const, group: 'more' as const },\n { id: 'languages', labelKey: 'languages', ns: 'sections' as const, group: 'more' as const },\n {\n id: 'other-profiles',\n labelKey: 'otherProfiles',\n ns: 'sections' as const,\n group: 'more' as const,\n },\n] as const;\n\n/** Every profile section id, in canonical render order. */\nexport type SectionId = (typeof ALL_SECTIONS)[number]['id'];\n\n/**\n * Whether a section has content a visitor can see (used to hide empty nav\n * entries and sections for visitors). Items the owner hid do not count: a\n * section whose only entries are hidden renders nothing for a visitor, so it\n * must not get a nav entry either.\n */\nexport function isSectionPopulated(profile: Profile, id: string): boolean {\n const hasVisible = <T extends { hidden?: boolean }>(items: T[] | undefined): boolean =>\n filterHidden(items).length > 0;\n switch (id) {\n case 'about':\n // Text fields, not hideable items. Without a headline the summary shows\n // in the identity card instead, so the About section stays out of it.\n return Boolean(profile.about && profile.headline);\n case 'career':\n return hasVisible(profile.positions);\n case 'education':\n return hasVisible(profile.education);\n case 'courses':\n return hasVisible(profile.courses);\n case 'skills':\n // Skills have no owner-hide flag (`ProfileSkill` carries no `hidden`).\n return Boolean(profile.skills?.length);\n case 'projects':\n return hasVisible(profile.projects);\n case 'credentials':\n return hasVisible(profile.certifications);\n case 'publications':\n return hasVisible(profile.publications);\n case 'presentations':\n return hasVisible(profile.presentations) || hasVisible(profile.presentationDeliveries);\n case 'involvement':\n return hasVisible(profile.involvement);\n case 'investments':\n return hasVisible(profile.investments);\n case 'awards':\n return hasVisible(profile.honors);\n case 'languages':\n return hasVisible(profile.languages);\n case 'other-profiles':\n return hasVisible(profile.externalAccounts);\n default:\n return false;\n }\n}\n\n/**\n * The section ids a given viewer should see, in {@link ALL_SECTIONS} order.\n * Owners see every section; visitors only see populated ones.\n */\nexport function getVisibleSectionIds(profile: Profile, isOwnProfile: boolean): SectionId[] {\n return ALL_SECTIONS.map((s) => s.id).filter(\n (id) => isOwnProfile || isSectionPopulated(profile, id),\n );\n}\n\n/** Drop items the owner has hidden from visitors. */\nexport function filterHidden<T extends { hidden?: boolean }>(items: T[] | undefined): T[] {\n return (items ?? []).filter((i) => !i.hidden);\n}\n\n/**\n * Items visible to a given viewer: owners see their own hidden items, visitors\n * do not. Shared by every export format (markdown, docx, print) and the public\n * `.md` route so the hidden rule lives in one place.\n */\nexport function visibleItems<T extends { hidden?: boolean }>(\n items: T[] | undefined,\n isOwnProfile: boolean,\n): T[] {\n return isOwnProfile ? (items ?? []) : filterHidden(items);\n}\n\n/**\n * English heading text for each section, keyed by {@link SectionId} (e.g.\n * `other-profiles` renders as \"Links\"). Exhaustive `Record<SectionId, string>`:\n * adding a section to {@link ALL_SECTIONS} without a label here is a compile\n * error, so a section can never silently render without (or with a stale)\n * heading. Consumers with i18n resolve their own localized labels via\n * `labelKey`; this is the canonical English fallback used by the exports.\n */\nexport const SECTION_LABELS: Record<SectionId, string> = {\n about: 'About',\n career: 'Career',\n skills: 'Skills',\n projects: 'Projects',\n presentations: 'Talks & sessions',\n publications: 'Publications',\n credentials: 'Credentials',\n education: 'Education',\n courses: 'Courses',\n awards: 'Awards',\n involvement: 'Involvement',\n investments: 'Investments',\n languages: 'Languages',\n 'other-profiles': 'Links',\n};\n","/** Shared URL helpers for the JSON-LD emitters. */\n\nexport const DEFAULT_BASE_URL = 'https://sifa.id';\n\n/**\n * Drop trailing slashes from a caller-supplied base URL.\n *\n * Written as a scan rather than `replace(/\\/+$/, '')` on purpose: the regex\n * form is a polynomial-backtracking hazard on input the library does not\n * control (CodeQL js/polynomial-redos). This runs in linear time.\n */\nexport function normaliseBaseUrl(baseUrl: string | undefined): string {\n const value = baseUrl ?? DEFAULT_BASE_URL;\n let end = value.length;\n while (end > 0 && value.charCodeAt(end - 1) === 0x2f) end -= 1;\n return value.slice(0, end);\n}\n\nexport function profileUrl(baseUrl: string, handle: string): string {\n return `${baseUrl}/p/${handle}`;\n}\n","/**\n * JSON Resume v1.0.0 emitter for a Sifa profile.\n *\n * https://jsonresume.org/schema/\n *\n * One pure function, no fetching, no DOM, no framework -- the same shape as the\n * JSON-LD emitter next door, and for the same reason: sifa.id, page.sifa.id and\n * any future consumer should emit one document from one implementation.\n *\n * Why this format earns its place: it is the lingua franca of CV tooling, so\n * emitting it hands a user every JSON Resume theme, and the Typst CV templates\n * on Typst Universe, without Sifa writing a renderer for any of them.\n *\n * Fields Sifa holds that JSON Resume has no slot for -- endorsements,\n * confirmations, involvement, investments, presentations, courses -- are\n * dropped rather than smuggled into a nearby field. The loss is documented for\n * the user at the point of download.\n */\n\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport type {\n ExternalAccount,\n LanguageProficiency,\n LocationValue,\n ProfileCertification,\n ProfileEducation,\n ProfileHonor,\n ProfileLanguage,\n ProfilePosition,\n ProfileProject,\n ProfilePublication,\n ProfileSkill,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normaliseBaseUrl, profileUrl } from '../jsonld/url.js';\nimport { normalizeExportContact, type ExportContact } from './export-contact.js';\n\nexport type Sanitizer = (input: string) => string;\n\nexport interface JsonResumeOptions {\n /** Canonical origin for the profile URL. Defaults to `https://sifa.id`. */\n readonly baseUrl?: string;\n /** Applied to every user-authored string. Defaults to identity. */\n readonly sanitize?: Sanitizer;\n /**\n * Absolute URL of the profile this resume describes. Defaults to\n * `${baseUrl}/p/${handle}`. A personal site served from a custom domain is\n * not at that path.\n */\n readonly canonicalUrl?: string;\n /**\n * Email, phone and postal address to print. Sifa holds none of these; the\n * owner types them on the export page. Without it the resume has no\n * `email`, `phone` or `location.address`. Personal details in the same object\n * (driver's license, right to work, nationality) are left out: JSON Resume\n * has no field for them, and strict consumers such as altacv refuse a file\n * with an unknown key.\n */\n readonly contact?: ExportContact;\n}\n\n/**\n * The subset of `Profile` this emitter reads. Deliberately structural rather\n * than the full `Profile`, so a caller holding a partially hydrated profile --\n * or a test -- does not have to invent forty unrelated fields.\n */\nexport interface JsonResumeProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly avatar?: string;\n readonly headline?: string;\n readonly about?: string;\n readonly website?: string;\n readonly location?: LocationValue | null;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly skills?: ProfileSkill[];\n readonly certifications?: ProfileCertification[];\n readonly projects?: ProfileProject[];\n readonly publications?: ProfilePublication[];\n readonly volunteering?: ProfileVolunteering[];\n readonly honors?: ProfileHonor[];\n readonly languages?: ProfileLanguage[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface JsonResumeLocation {\n /** Street address; lines are separated by `\\n`, as the schema asks. */\n readonly address?: string;\n readonly city?: string;\n readonly region?: string;\n readonly countryCode?: string;\n readonly postalCode?: string;\n}\n\nexport interface JsonResumeProfileLink {\n readonly network: string;\n readonly username?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumeBasics {\n readonly name: string;\n readonly label?: string;\n readonly image?: string;\n readonly url?: string;\n readonly email?: string;\n readonly phone?: string;\n readonly summary?: string;\n readonly location?: JsonResumeLocation;\n readonly profiles?: JsonResumeProfileLink[];\n}\n\nexport interface JsonResumeWork {\n readonly name?: string;\n readonly position: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeVolunteer {\n /**\n * Omitted when the record carries no organization name at all. Emitting an\n * empty string would render as a blank organization line in a CV template.\n */\n readonly organization?: string;\n readonly position?: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeEducation {\n /** Omitted when the record carries no institution name. See volunteer. */\n readonly institution?: string;\n readonly area?: string;\n readonly studyType?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeAward {\n readonly title: string;\n readonly date?: string;\n readonly awarder?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeCertificate {\n readonly name: string;\n readonly date?: string;\n readonly issuer?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumePublication {\n readonly name: string;\n readonly publisher?: string;\n readonly releaseDate?: string;\n readonly url?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeSkill {\n readonly name: string;\n}\n\nexport interface JsonResumeLanguage {\n readonly language: string;\n readonly fluency?: string;\n}\n\nexport interface JsonResumeProject {\n readonly name: string;\n readonly description?: string;\n readonly url?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeMeta {\n readonly canonical: string;\n readonly version: string;\n readonly lastModified?: string;\n}\n\nexport interface JsonResume {\n readonly basics?: JsonResumeBasics;\n readonly work?: JsonResumeWork[];\n readonly volunteer?: JsonResumeVolunteer[];\n readonly education?: JsonResumeEducation[];\n readonly awards?: JsonResumeAward[];\n readonly certificates?: JsonResumeCertificate[];\n readonly publications?: JsonResumePublication[];\n readonly skills?: JsonResumeSkill[];\n readonly languages?: JsonResumeLanguage[];\n readonly projects?: JsonResumeProject[];\n readonly meta?: JsonResumeMeta;\n}\n\n/** Schema version this emitter targets, as JSON Resume writes it in `meta`. */\nconst SCHEMA_VERSION = 'v1.0.0';\n\n/**\n * A date JSON Resume will accept: `YYYY`, `YYYY-MM` or `YYYY-MM-DD`. Sifa\n * stores RFC 3339 for some fields, and a full timestamp pasted into a CV\n * template renders as `2020-03-04T00:00:00.000Z` on the page.\n */\nconst RESUME_DATE_RE = /^(\\d{4})(-\\d{2})?(-\\d{2})?/;\n\nexport function toResumeDate(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n if (!trimmed) return undefined;\n const match = RESUME_DATE_RE.exec(trimmed);\n return match ? match[0] : undefined;\n}\n\n/**\n * JSON Resume's `fluency` is free text, so these are display labels rather\n * than a mapped vocabulary. Kept in one place so export and a future import\n * agree on the spelling.\n */\nconst FLUENCY_LABELS: Record<LanguageProficiency, string> = {\n elementary: 'Elementary',\n limited_working: 'Limited working',\n professional_working: 'Professional working',\n full_professional: 'Full professional',\n native: 'Native or bilingual',\n};\n\nconst identity: Sanitizer = (input) => input;\n\n/** Drops keys whose value is undefined, so the document carries no empty slots. */\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\n/** Adds the owner's street address to the profile's location. */\nfunction withAddress(\n location: JsonResumeLocation | undefined,\n address: string | undefined,\n): JsonResumeLocation | undefined {\n return address ? { address, ...location } : location;\n}\n\n/** Omits the key entirely when the mapped list came out empty. */\nfunction section<K extends string, T>(key: K, items: T[]): Record<K, T[]> | undefined {\n return items.length > 0 ? ({ [key]: items } as Record<K, T[]>) : undefined;\n}\n\nfunction mapLocation(\n location: LocationValue | null | undefined,\n s: Sanitizer,\n): JsonResumeLocation | undefined {\n if (!location) return undefined;\n const mapped = compact({\n city: text(location.locality ?? location.city, s),\n region: text(location.region, s),\n countryCode: text(location.countryCode, s),\n postalCode: text(location.postalCode, s),\n });\n return Object.keys(mapped).length > 0 ? mapped : undefined;\n}\n\n/** Trimmed, sanitised, and undefined when there is nothing left. */\nfunction text(value: string | undefined, s: Sanitizer): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? s(trimmed) : undefined;\n}\n\n/**\n * The name to print for a linked organization. `entityName` is the AppView's\n * resolved current name and wins when present, matching how the profile itself\n * renders these.\n */\nfunction orgName(\n entityName: string | undefined,\n stored: string | undefined,\n s: Sanitizer,\n): string | undefined {\n return text(entityName, s) ?? text(stored, s);\n}\n\n/**\n * Build a JSON Resume v1.0.0 document from a Sifa profile.\n *\n * Hidden records are dropped. Keys are omitted rather than emitted empty, so a\n * profile with no publications produces a document with no `publications` key\n * instead of one a reader has to interpret.\n */\nexport function profileToJsonResume(\n profile: JsonResumeProfileInput,\n options: JsonResumeOptions = {},\n): JsonResume {\n const s = options.sanitize ?? identity;\n const baseUrl = normaliseBaseUrl(options.baseUrl);\n const canonical = options.canonicalUrl ?? profileUrl(baseUrl, profile.handle);\n const contact = normalizeExportContact(options.contact ?? {});\n\n const name =\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName, s) ??\n profile.handle;\n\n const profiles = filterHidden(profile.externalAccounts).map((account) =>\n compact<JsonResumeProfileLink>({\n network: s(account.platform),\n username: text(account.label, s),\n url: text(account.url, s),\n }),\n );\n\n const basics = compact<JsonResumeBasics>({\n name,\n label: text(profile.headline, s),\n image: text(profile.avatar, s),\n email: text(contact.email, s),\n phone: text(contact.phone, s),\n url: text(profile.website, s) ?? canonical,\n summary: text(profile.about, s),\n location: withAddress(mapLocation(profile.location, s), text(contact.address, s)),\n ...section('profiles', profiles),\n });\n\n const work = filterHidden(profile.positions).map((position) =>\n compact<JsonResumeWork>({\n name: orgName(position.entityName, position.company, s),\n position: s(position.title),\n summary: text(position.description, s),\n startDate: toResumeDate(position.startedAt),\n endDate: toResumeDate(position.endedAt),\n }),\n );\n\n const volunteer = filterHidden(profile.volunteering).map((entry) =>\n compact<JsonResumeVolunteer>({\n organization: orgName(entry.entityName, entry.organization, s),\n position: text(entry.role, s),\n summary: text(entry.description, s),\n startDate: toResumeDate(entry.startDate),\n endDate: toResumeDate(entry.endDate),\n }),\n );\n\n const education = filterHidden(profile.education).map((entry) =>\n compact<JsonResumeEducation>({\n institution: orgName(entry.entityName, entry.institution, s),\n area: text(entry.fieldOfStudy, s),\n studyType: text(entry.degree, s),\n startDate: toResumeDate(entry.startedAt),\n endDate: toResumeDate(entry.endedAt),\n }),\n );\n\n const awards = filterHidden(profile.honors).map((honor) =>\n compact<JsonResumeAward>({\n title: s(honor.title),\n date: toResumeDate(honor.date),\n awarder: orgName(honor.entityName, honor.issuer, s),\n summary: text(honor.description, s),\n }),\n );\n\n const certificates = filterHidden(profile.certifications).map((cert) =>\n compact<JsonResumeCertificate>({\n name: s(cert.name),\n date: toResumeDate(cert.issueDate),\n // `issuingOrg` is the deprecated read-view alias for `authority`; records\n // indexed before the rename carry only the alias.\n issuer: orgName(cert.entityName, cert.authority ?? cert.issuingOrg, s),\n url: text(cert.credentialUrl, s),\n }),\n );\n\n const publications = filterHidden(profile.publications).map((publication) =>\n compact<JsonResumePublication>({\n name: s(publication.title),\n publisher: text(publication.publisher, s),\n releaseDate: toResumeDate(publication.date),\n url: text(publication.url, s),\n summary: text(publication.description, s),\n }),\n );\n\n // One entry per skill rather than JSON Resume's grouped shape: Sifa stores one\n // record per skill, and keeping the two symmetric is what makes a round-trip\n // through a third-party editor safe. Category and endorsement counts have no\n // JSON Resume slot and are dropped. Skill records carry no `hidden` flag, so\n // there is nothing to filter here.\n const skills = (profile.skills ?? []).map((skill) => ({ name: s(skill.name) }));\n\n const languages = filterHidden(profile.languages).map((language) =>\n compact<JsonResumeLanguage>({\n language: s(language.language),\n fluency: language.proficiency ? FLUENCY_LABELS[language.proficiency] : undefined,\n }),\n );\n\n const projects = filterHidden(profile.projects).map((project) =>\n compact<JsonResumeProject>({\n name: s(project.name),\n description: text(project.description, s),\n url: text(project.url, s),\n startDate: toResumeDate(project.startDate),\n endDate: toResumeDate(project.endDate),\n }),\n );\n\n return {\n basics,\n ...section('work', work),\n ...section('volunteer', volunteer),\n ...section('education', education),\n ...section('awards', awards),\n ...section('certificates', certificates),\n ...section('publications', publications),\n ...section('skills', skills),\n ...section('languages', languages),\n ...section('projects', projects),\n meta: { canonical, version: SCHEMA_VERSION },\n };\n}\n","import type { LocationValue } from '../types/index.js';\n\n/**\n * Format a LocationValue for display.\n *\n * Prefers `locality` (the community.lexicon.location.address field) and\n * falls back to the legacy `city` slot so values produced before the\n * sifa-api alias migration still render correctly during the additive\n * transition window.\n */\nexport function formatLocation(loc: LocationValue | null | undefined): string {\n if (!loc) return '';\n const locality = loc.locality ?? loc.city;\n const parts = [locality, loc.region, loc.country].filter(Boolean);\n if (loc.postalCode && !locality) {\n return `${loc.postalCode}, ${loc.country}`;\n }\n return parts.join(', ');\n}\n\n/** Parse a display string back into a LocationValue (best-effort for legacy data) */\nexport function parseLocationString(str: string): LocationValue | null {\n if (!str.trim()) return null;\n const parts = str\n .split(',')\n .map((p) => p.trim())\n .filter(Boolean);\n if (parts.length >= 3) {\n return {\n city: parts[0],\n region: parts[1],\n country: parts[parts.length - 1]!,\n };\n }\n if (parts.length === 2) {\n return {\n city: parts[0],\n country: parts[1]!,\n };\n }\n return { country: parts[0]! };\n}\n\n/** Convert ISO 3166-1 alpha-2 country code to flag emoji */\nexport function countryCodeToFlag(code: string | undefined): string {\n if (!code || code.length !== 2) return '';\n const upper = code.toUpperCase();\n const a = upper.codePointAt(0);\n const b = upper.codePointAt(1);\n if (a === undefined || b === undefined) return '';\n return String.fromCodePoint(a - 0x41 + 0x1f1e6, b - 0x41 + 0x1f1e6);\n}\n","/**\n * Course-role taxonomy. Mirrors `id.sifa.defs#courseRole.knownValues` from\n * sifa-lexicons. A course record without a role is a course the user took;\n * taught and assisted courses render under the \"Teaching\" heading.\n */\n\nexport interface CourseRoleOption {\n value: string;\n label: string;\n}\n\nexport const COURSE_ROLE_TAKEN = 'id.sifa.defs#courseTaken';\nexport const COURSE_ROLE_TAUGHT = 'id.sifa.defs#courseTaught';\nexport const COURSE_ROLE_TEACHING_ASSISTANT = 'id.sifa.defs#courseTeachingAssistant';\n\nexport const COURSE_ROLE_OPTIONS: CourseRoleOption[] = [\n { value: COURSE_ROLE_TAKEN, label: 'Student' },\n { value: COURSE_ROLE_TAUGHT, label: 'Instructor' },\n { value: COURSE_ROLE_TEACHING_ASSISTANT, label: 'Teaching assistant' },\n];\n\nexport const COURSE_ROLE_LABELS: Record<string, string> = Object.fromEntries(\n COURSE_ROLE_OPTIONS.map((o) => [o.value, o.label]),\n);\n\n/**\n * Resolve the label for a course-role token. An unknown token resolves to\n * `undefined` rather than the raw value, so a token a client has not learned\n * never shows up as a raw `id.sifa.defs#...` string.\n */\nexport function getCourseRoleLabel(value: string | undefined | null): string | undefined {\n if (!value) return undefined;\n return COURSE_ROLE_LABELS[value];\n}\n\nconst TEACHING_ROLES = new Set([COURSE_ROLE_TAUGHT, COURSE_ROLE_TEACHING_ASSISTANT]);\n\n/**\n * Whether a course is one the user taught or assisted with. An absent, taken,\n * or unknown role counts as taken, so a record is never dropped from the\n * profile when a client meets a token it has not learned.\n */\nexport function isTeachingCourse(course: { role?: string | null }): boolean {\n return !!course.role && TEACHING_ROLES.has(course.role);\n}\n\n/** Split courses into the ones taken and the ones taught or assisted, keeping order. */\nexport function splitCoursesByRole<T extends { role?: string | null }>(\n courses: readonly T[],\n): { taken: T[]; teaching: T[] } {\n const taken: T[] = [];\n const teaching: T[] = [];\n for (const c of courses) (isTeachingCourse(c) ? teaching : taken).push(c);\n return { taken, teaching };\n}\n","/**\n * Funder-format CV documents: the NIH Biographical Sketch (Common Form plus\n * NIH Supplement) and the ERC Part B1 \"Curriculum vitae and Track Record\".\n *\n * A pure mapper from a Sifa profile and the owner's pick of items to a plain\n * JSON document a typesetter lays out. Like the JSON Resume emitter next door,\n * nothing here fetches or renders.\n *\n * Funders cap how many items a CV may list. The caps live in\n * `FUNDER_CV_GROUPS`, so the export page that offers the choice and the server\n * that enforces it read one number. Sections a funder asks for that Sifa holds\n * no records for (a personal statement, contributions to science, career\n * breaks) are emitted as `null` or empty, for the template to show as a\n * placeholder the applicant fills in.\n */\n\nimport { z } from 'zod';\n\nimport { formatLocation } from '../format/location-utils.js';\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport { isTeachingCourse } from '../taxonomy/course-role.js';\nimport type {\n ExternalAccount,\n ProfileCourse,\n ProfileEducation,\n ProfileHonor,\n ProfilePosition,\n ProfilePresentation,\n ProfilePublication,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normalizeExportContact, type ExportContact } from './export-contact.js';\nimport { toResumeDate } from './json-resume.js';\n\nexport const FUNDER_CV_FORMATS = ['nih', 'erc'] as const;\nexport type FunderCvFormat = (typeof FUNDER_CV_FORMATS)[number];\n\nexport const FUNDER_CV_ITEM_KINDS = ['publication', 'presentation', 'honor'] as const;\nexport type FunderCvItemKind = (typeof FUNDER_CV_ITEM_KINDS)[number];\n\nexport interface FunderCvGroupSpec {\n readonly kinds: readonly FunderCvItemKind[];\n /** Most items the funder's form takes for this group. */\n readonly max: number;\n}\n\n/**\n * The pickable groups per format, with the funder's item limits.\n *\n * - NIH Common Form \"Products\": up to 5 closely related to the proposed\n * project and up to 5 other significant products. Publications, conference\n * papers and presentations all count. NIH Supplement \"Honors\": up to 15.\n * - ERC Part B1 \"Research achievements\": up to ten research outputs. \"Peer\n * recognition\" is \"selected examples\" with no number; 15 is Sifa's own cap,\n * to keep the section inside the 4-page limit.\n */\nexport const FUNDER_CV_GROUPS = {\n nih: {\n related: { kinds: ['publication', 'presentation'], max: 5 },\n other: { kinds: ['publication', 'presentation'], max: 5 },\n honors: { kinds: ['honor'], max: 15 },\n },\n erc: {\n outputs: { kinds: ['publication'], max: 10 },\n recognition: { kinds: ['honor', 'presentation'], max: 15 },\n },\n} as const satisfies Record<FunderCvFormat, Record<string, FunderCvGroupSpec>>;\n\nexport type FunderCvGroupName<F extends FunderCvFormat> = keyof (typeof FUNDER_CV_GROUPS)[F] &\n string;\n\n/** A picked item: `<kind>:<rkey>`, e.g. `publication:3kxyz`. */\nexport function funderCvItemId(kind: FunderCvItemKind, rkey: string): string {\n return `${kind}:${rkey}`;\n}\n\n/** Record key syntax (atproto): 1-512 of A-Z a-z 0-9 . - _ : ~ */\nconst ITEM_ID_RE = /^(publication|presentation|honor):[A-Za-z0-9._~:-]{1,512}$/;\n\nfunction kindOf(id: string): FunderCvItemKind | undefined {\n const kind = id.slice(0, id.indexOf(':'));\n return (FUNDER_CV_ITEM_KINDS as readonly string[]).includes(kind)\n ? (kind as FunderCvItemKind)\n : undefined;\n}\n\nfunction groupSchema(spec: FunderCvGroupSpec) {\n return z\n .array(\n z\n .string()\n .regex(ITEM_ID_RE)\n .refine((id) => spec.kinds.includes(kindOf(id) as FunderCvItemKind), {\n message: `Only ${spec.kinds.join(', ')} items fit here`,\n }),\n )\n .max(spec.max)\n .default([]);\n}\n\nfunction noRepeats(lists: readonly string[][]): boolean {\n const all = lists.flat();\n return new Set(all).size === all.length;\n}\n\nconst nihSelectionSchema = z\n .object({\n format: z.literal('nih'),\n related: groupSchema(FUNDER_CV_GROUPS.nih.related),\n other: groupSchema(FUNDER_CV_GROUPS.nih.other),\n honors: groupSchema(FUNDER_CV_GROUPS.nih.honors),\n })\n .refine((s) => noRepeats([s.related, s.other, s.honors]), {\n message: 'An item can be listed once',\n });\n\nconst ercSelectionSchema = z\n .object({\n format: z.literal('erc'),\n outputs: groupSchema(FUNDER_CV_GROUPS.erc.outputs),\n recognition: groupSchema(FUNDER_CV_GROUPS.erc.recognition),\n })\n .refine((s) => noRepeats([s.outputs, s.recognition]), {\n message: 'An item can be listed once',\n });\n\n/**\n * The owner's pick for one format. Enforces the shape and the funder's limits;\n * whether each id is one of the owner's own visible records is a separate\n * check against the profile, `findUnknownFunderCvItems`.\n */\nexport const funderCvSelectionSchema = z.discriminatedUnion('format', [\n nihSelectionSchema,\n ercSelectionSchema,\n]);\n\nexport type FunderCvSelection = z.infer<typeof funderCvSelectionSchema>;\n\n/** The subset of `Profile` the funder mapper reads. */\nexport interface FunderCvProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly headline?: string;\n readonly website?: string;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly publications?: ProfilePublication[];\n readonly presentations?: ProfilePresentation[];\n readonly honors?: ProfileHonor[];\n readonly courses?: ProfileCourse[];\n readonly volunteering?: ProfileVolunteering[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface FunderCvCandidate {\n readonly id: string;\n readonly kind: FunderCvItemKind;\n readonly title: string;\n /** `YYYY`, `YYYY-MM` or `YYYY-MM-DD`; absent when the record has no date. */\n readonly date?: string;\n}\n\n/** The most recent visible delivery of a talk, which dates it. */\nfunction latestDeliveryRecord(presentation: ProfilePresentation) {\n const dateOf = (d: { date?: string | null }) => toResumeDate(d.date ?? undefined) ?? '';\n return [...filterHidden(presentation.deliveries)]\n .sort((a, b) => dateOf(a).localeCompare(dateOf(b)))\n .at(-1);\n}\n\nfunction latestDelivery(presentation: ProfilePresentation): string | undefined {\n return toResumeDate(latestDeliveryRecord(presentation)?.date ?? undefined);\n}\n\nfunction candidatesOf(profile: FunderCvProfileInput, kind: FunderCvItemKind): FunderCvCandidate[] {\n switch (kind) {\n case 'publication':\n return filterHidden(profile.publications).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: toResumeDate(p.date),\n }));\n case 'presentation':\n return filterHidden(profile.presentations).map((p) => ({\n id: funderCvItemId(kind, p.rkey),\n kind,\n title: p.title,\n date: latestDelivery(p),\n }));\n case 'honor':\n return filterHidden(profile.honors).map((h) => ({\n id: funderCvItemId(kind, h.rkey),\n kind,\n title: h.title,\n date: toResumeDate(h.date),\n }));\n }\n}\n\n/**\n * The owner's visible items of the given kinds, most recent first. Undated\n * items go last, in profile order. The sort is stable, so equal dates keep\n * profile order too.\n */\nexport function listFunderCvCandidates(\n profile: FunderCvProfileInput,\n kinds: readonly FunderCvItemKind[],\n): FunderCvCandidate[] {\n const all = kinds.flatMap((kind) => candidatesOf(profile, kind));\n return all.sort((a, b) => {\n if (a.date && b.date) return b.date.localeCompare(a.date);\n if (a.date) return -1;\n if (b.date) return 1;\n return 0;\n });\n}\n\n/**\n * What the export page pre-ticks: the most recent publications up to each\n * limit (for NIH, the first five as closely related and the next five as\n * other), and the most recent honors. Talks are offered but not pre-ticked.\n */\nexport function defaultFunderCvSelection(\n profile: FunderCvProfileInput,\n format: FunderCvFormat,\n): FunderCvSelection {\n const publications = listFunderCvCandidates(profile, ['publication']).map((c) => c.id);\n const honors = listFunderCvCandidates(profile, ['honor']).map((c) => c.id);\n if (format === 'nih') {\n const { related, other } = FUNDER_CV_GROUPS.nih;\n return {\n format,\n related: publications.slice(0, related.max),\n other: publications.slice(related.max, related.max + other.max),\n honors: honors.slice(0, FUNDER_CV_GROUPS.nih.honors.max),\n };\n }\n return {\n format,\n outputs: publications.slice(0, FUNDER_CV_GROUPS.erc.outputs.max),\n recognition: honors.slice(0, FUNDER_CV_GROUPS.erc.recognition.max),\n };\n}\n\nfunction selectedIds(selection: FunderCvSelection): string[] {\n return selection.format === 'nih'\n ? [...selection.related, ...selection.other, ...selection.honors]\n : [...selection.outputs, ...selection.recognition];\n}\n\n/**\n * Ids in the selection that are not visible records on this profile. The\n * profile is the owner's own, so an empty result means every pick is theirs.\n */\nexport function findUnknownFunderCvItems(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n): string[] {\n const known = new Set(listFunderCvCandidates(profile, FUNDER_CV_ITEM_KINDS).map((c) => c.id));\n return selectedIds(selection).filter((id) => !known.has(id));\n}\n\n// ---------------------------------------------------------------------------\n// Document shapes\n// ---------------------------------------------------------------------------\n\nexport interface FunderCvPerson {\n /** Full name as the profile shows it; the handle when there is no name. */\n readonly name: string;\n readonly givenName?: string;\n readonly familyName?: string;\n /** Bare ORCID iD, e.g. `0000-0002-1825-0097`. */\n readonly orcid?: string;\n readonly website?: string;\n}\n\n/**\n * The ERC person, with the contact and personal details the owner typed for\n * this export. The NIH Biographical Sketch has no such block, so only ERC carries them.\n */\nexport interface ErcFunderCvPerson extends FunderCvPerson {\n readonly email?: string;\n readonly phone?: string;\n /** Lines are separated by `\\n`. */\n readonly address?: string;\n readonly driversLicense?: string;\n readonly rightToWork?: string;\n readonly nationality?: string;\n}\n\nexport interface FunderCvOptions {\n /**\n * Contact details the owner typed for this export. Sifa stores none of\n * them. Used for ERC only.\n */\n readonly contact?: ExportContact;\n}\n\n/** One product or research output, ready to cite. */\nexport interface FunderCvCitation {\n readonly kind: 'publication' | 'presentation';\n readonly title: string;\n /** Authors in record order, comma separated. For a talk, the speaker. */\n readonly authors?: string;\n /** Journal, publisher or event. */\n readonly venue?: string;\n /** Year. */\n readonly date?: string;\n readonly doi?: string;\n readonly url?: string;\n}\n\nexport interface FunderCvHonor {\n readonly year?: string;\n readonly title: string;\n readonly organization?: string;\n}\n\nexport interface NihEducation {\n readonly organization: string;\n readonly location?: string;\n readonly degree?: string;\n /** Start date, `MM/YYYY` or `YYYY`. */\n readonly start?: string;\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly end?: string;\n readonly field?: string;\n}\n\nexport interface FunderCvAppointment {\n /** `YYYY`. */\n readonly start?: string;\n /** `YYYY`, or null for a current appointment. */\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n readonly location?: string;\n}\n\n/** NIH Biographical Sketch: Common Form plus NIH Supplement. */\nexport interface NihFunderCv {\n readonly format: 'nih';\n readonly person: FunderCvPerson;\n readonly positionTitle?: string;\n /** Primary organization: the current position's. */\n readonly organization?: string;\n readonly location?: string;\n readonly education: NihEducation[];\n readonly appointments: FunderCvAppointment[];\n readonly products: { readonly related: FunderCvCitation[]; readonly other: FunderCvCitation[] };\n readonly honors: FunderCvHonor[];\n /** Supplement \"Personal Statement\". Sifa holds no record for it. */\n readonly personalStatement: string | null;\n /** Supplement \"Contributions to Science\". Sifa holds no record for them. */\n readonly contributions: string[];\n}\n\nexport interface ErcEducation {\n /** Receipt date, `MM/YYYY` or `YYYY`. */\n readonly date?: string;\n readonly degree?: string;\n readonly field?: string;\n readonly institution: string;\n}\n\nexport interface ErcRecognition {\n readonly kind: 'honor' | 'presentation';\n readonly year?: string;\n readonly title: string;\n /** Awarding body, or the event a talk was given at. */\n readonly organization?: string;\n}\n\nexport interface ErcContribution {\n readonly kind: 'teaching' | 'service';\n readonly start?: string;\n readonly end: string | null;\n readonly title: string;\n readonly organization?: string;\n}\n\n/** ERC Part B1, section \"Curriculum vitae and Track Record\". */\nexport interface ErcFunderCv {\n readonly format: 'erc';\n readonly person: ErcFunderCvPerson;\n readonly education: ErcEducation[];\n readonly currentPositions: FunderCvAppointment[];\n readonly previousPositions: FunderCvAppointment[];\n readonly outputs: FunderCvCitation[];\n readonly recognition: ErcRecognition[];\n readonly communityContributions: ErcContribution[];\n /** Proposal acronym for the page header. Not a profile fact. */\n readonly acronym: string | null;\n /** \"Career breaks, diverse career paths and major life events\". */\n readonly careerBreaks: string | null;\n}\n\nexport type FunderCvDocument = NihFunderCv | ErcFunderCv;\n\n// ---------------------------------------------------------------------------\n// Mapping\n// ---------------------------------------------------------------------------\n\nfunction text(value: string | null | undefined): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? trimmed : undefined;\n}\n\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\nfunction year(value: string | null | undefined): string | undefined {\n return toResumeDate(value ?? undefined)?.slice(0, 4);\n}\n\n/** `MM/YYYY` when the month is known, else `YYYY`. */\nfunction monthYear(value: string | null | undefined): string | undefined {\n const date = toResumeDate(value ?? undefined);\n if (!date) return undefined;\n return date.length >= 7 ? `${date.slice(5, 7)}/${date.slice(0, 4)}` : date;\n}\n\n/** Newest start first; undated entries last, in profile order. */\nfunction byStartDesc<T>(items: T[], start: (item: T) => string | undefined): T[] {\n return [...items].sort((a, b) => {\n const da = toResumeDate(start(a));\n const db = toResumeDate(start(b));\n if (da && db) return db.localeCompare(da);\n if (da) return -1;\n if (db) return 1;\n return 0;\n });\n}\n\nconst ORCID_RE = /\\d{4}-\\d{4}-\\d{4}-\\d{3}[\\dX]/;\n\nfunction orcidOf(accounts: ExternalAccount[] | undefined): string | undefined {\n const account = filterHidden(accounts).find((a) => a.platform.toLowerCase() === 'orcid');\n return account ? ORCID_RE.exec(account.url)?.[0] : undefined;\n}\n\nfunction personOf(profile: FunderCvProfileInput): FunderCvPerson {\n return compact<FunderCvPerson>({\n name:\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName) ??\n profile.handle,\n givenName: text(profile.givenName),\n familyName: text(profile.familyName),\n orcid: orcidOf(profile.externalAccounts),\n website: text(profile.website),\n });\n}\n\nfunction appointment(position: ProfilePosition): FunderCvAppointment {\n return compact<FunderCvAppointment>({\n start: year(position.startedAt),\n end: year(position.endedAt) ?? null,\n title: position.title,\n organization: text(position.entityName) ?? text(position.company),\n location: text(formatLocation(position.location)),\n });\n}\n\n/** The current position to name on the form: the flagged primary, else the newest. */\nfunction currentPosition(positions: ProfilePosition[]): ProfilePosition | undefined {\n const current = byStartDesc(\n positions.filter((p) => !text(p.endedAt)),\n (p) => p.startedAt,\n );\n return current.find((p) => p.primary) ?? current[0];\n}\n\nfunction lookup(profile: FunderCvProfileInput) {\n const publications = new Map(filterHidden(profile.publications).map((p) => [p.rkey, p]));\n const presentations = new Map(filterHidden(profile.presentations).map((p) => [p.rkey, p]));\n const honors = new Map(filterHidden(profile.honors).map((h) => [h.rkey, h]));\n const split = (id: string) => {\n const at = id.indexOf(':');\n return { kind: id.slice(0, at), rkey: id.slice(at + 1) };\n };\n return { publications, presentations, honors, split };\n}\n\nfunction citations(\n ids: readonly string[],\n profile: FunderCvProfileInput,\n person: FunderCvPerson,\n): FunderCvCitation[] {\n const { publications, presentations, split } = lookup(profile);\n return ids.flatMap((id): FunderCvCitation[] => {\n const { kind, rkey } = split(id);\n if (kind === 'publication') {\n const p = publications.get(rkey);\n if (!p) return [];\n const authors = (p.contributors ?? []).map((c) => c.name.trim()).filter(Boolean);\n return [\n compact<FunderCvCitation>({\n kind: 'publication',\n title: p.title,\n authors: authors.length > 0 ? authors.join(', ') : undefined,\n venue: text(p.publisher),\n date: year(p.date),\n doi: text(p.doi),\n url: text(p.url),\n }),\n ];\n }\n if (kind === 'presentation') {\n const talk = presentations.get(rkey);\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<FunderCvCitation>({\n kind: 'presentation',\n title: talk.title,\n authors: person.name,\n venue: text(delivery?.eventName),\n date: year(delivery?.date),\n url: text(talk.links?.[0]?.uri),\n }),\n ];\n }\n return [];\n });\n}\n\nfunction honorsOf(ids: readonly string[], profile: FunderCvProfileInput): FunderCvHonor[] {\n const { honors, split } = lookup(profile);\n return ids.flatMap((id) => {\n const { kind, rkey } = split(id);\n const h = kind === 'honor' ? honors.get(rkey) : undefined;\n if (!h) return [];\n return [\n compact<FunderCvHonor>({\n year: year(h.date),\n title: h.title,\n organization: text(h.entityName) ?? text(h.issuer),\n }),\n ];\n });\n}\n\n/**\n * Build the funder document from the profile and the owner's selection.\n *\n * Hidden records never appear. Selected ids that match no visible record are\n * skipped; callers that must refuse them check `findUnknownFunderCvItems`\n * first. Selected items keep the order they were picked in.\n */\nexport function profileToFunderCv(\n profile: FunderCvProfileInput,\n selection: FunderCvSelection,\n options: FunderCvOptions = {},\n): FunderCvDocument {\n const person = personOf(profile);\n const positions = filterHidden(profile.positions);\n const education = byStartDesc(filterHidden(profile.education), (e) => e.startedAt);\n\n if (selection.format === 'nih') {\n const current = currentPosition(positions);\n return compact<NihFunderCv>({\n format: 'nih',\n person,\n positionTitle: text(current?.title),\n organization: current ? appointment(current).organization : undefined,\n location: text(formatLocation(current?.location)),\n education: education.map((e) =>\n compact<NihEducation>({\n organization: text(e.entityName) ?? e.institution,\n degree: text(e.degree),\n start: monthYear(e.startedAt),\n end: monthYear(e.endedAt),\n field: text(e.fieldOfStudy),\n }),\n ),\n appointments: byStartDesc(positions, (p) => p.startedAt).map(appointment),\n products: {\n related: citations(selection.related, profile, person),\n other: citations(selection.other, profile, person),\n },\n honors: honorsOf(selection.honors, profile),\n personalStatement: null,\n contributions: [],\n });\n }\n\n const appointments = byStartDesc(positions, (p) => p.startedAt);\n const { presentations, split } = lookup(profile);\n\n const recognition = selection.recognition.flatMap((id): ErcRecognition[] => {\n const { kind, rkey } = split(id);\n if (kind === 'honor') {\n const [honor] = honorsOf([id], profile);\n return honor ? [{ kind: 'honor', ...honor }] : [];\n }\n const talk = kind === 'presentation' ? presentations.get(rkey) : undefined;\n if (!talk) return [];\n const delivery = latestDeliveryRecord(talk);\n return [\n compact<ErcRecognition>({\n kind: 'presentation',\n year: year(delivery?.date),\n title: talk.title,\n organization: text(delivery?.eventName),\n }),\n ];\n });\n\n const teaching = filterHidden(profile.courses)\n .filter(isTeachingCourse)\n .map((c) =>\n compact<ErcContribution>({\n kind: 'teaching',\n start: year(c.startedAt),\n end: year(c.endedAt) ?? null,\n title: c.name,\n organization: text(c.entityName) ?? text(c.institution),\n }),\n );\n const service = filterHidden(profile.volunteering).map((v) =>\n compact<ErcContribution>({\n kind: 'service',\n start: year(v.startDate),\n end: year(v.endDate) ?? null,\n title: text(v.role) ?? text(v.entityName) ?? v.organization,\n organization: text(v.entityName) ?? text(v.organization),\n }),\n );\n\n return {\n format: 'erc',\n person: { ...person, ...normalizeExportContact(options.contact ?? {}) },\n education: education.map((e) =>\n compact<ErcEducation>({\n date: monthYear(e.endedAt) ?? monthYear(e.startedAt),\n degree: text(e.degree),\n field: text(e.fieldOfStudy),\n institution: text(e.entityName) ?? e.institution,\n }),\n ),\n currentPositions: appointments.filter((p) => !text(p.endedAt)).map(appointment),\n previousPositions: appointments.filter((p) => !!text(p.endedAt)).map(appointment),\n outputs: citations(selection.outputs, profile, person),\n recognition,\n communityContributions: [...teaching, ...service],\n acronym: null,\n careerBreaks: null,\n };\n}\n"]}
|
package/dist/resume/index.d.cts
CHANGED
|
@@ -3,7 +3,8 @@ import { L as LocationValue, d as ProfilePosition, f as ProfileEducation, P as P
|
|
|
3
3
|
import '../verification-providers-DeyGqaoC.cjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Private contact details for exports: email, phone
|
|
6
|
+
* Private contact and personal details for exports: email, phone, postal
|
|
7
|
+
* address, driver's license, right to work and nationality.
|
|
7
8
|
*
|
|
8
9
|
* Sifa never stores these and never writes them to the owner's PDS, where they
|
|
9
10
|
* would be public. The owner types them on the export page, the browser keeps
|
|
@@ -17,6 +18,9 @@ declare const EXPORT_CONTACT_LIMITS: {
|
|
|
17
18
|
readonly email: 254;
|
|
18
19
|
readonly phone: 40;
|
|
19
20
|
readonly address: 500;
|
|
21
|
+
readonly driversLicense: 60;
|
|
22
|
+
readonly rightToWork: 200;
|
|
23
|
+
readonly nationality: 100;
|
|
20
24
|
};
|
|
21
25
|
/** The contact details that go into an export. Absent keys are left out. */
|
|
22
26
|
interface ExportContact {
|
|
@@ -24,15 +28,26 @@ interface ExportContact {
|
|
|
24
28
|
readonly phone?: string;
|
|
25
29
|
/** Free text; lines are separated by `\n`. */
|
|
26
30
|
readonly address?: string;
|
|
31
|
+
/** License categories, e.g. "B, BE". */
|
|
32
|
+
readonly driversLicense?: string;
|
|
33
|
+
/** e.g. "EU citizen, no sponsorship needed". */
|
|
34
|
+
readonly rightToWork?: string;
|
|
35
|
+
readonly nationality?: string;
|
|
27
36
|
}
|
|
28
37
|
/** What the owner has typed, plus which fields they chose to include. */
|
|
29
38
|
interface ExportContactDraft {
|
|
30
39
|
readonly email?: string;
|
|
31
40
|
readonly phone?: string;
|
|
32
41
|
readonly address?: string;
|
|
42
|
+
readonly driversLicense?: string;
|
|
43
|
+
readonly rightToWork?: string;
|
|
44
|
+
readonly nationality?: string;
|
|
33
45
|
readonly includeEmail?: boolean;
|
|
34
46
|
readonly includePhone?: boolean;
|
|
35
47
|
readonly includeAddress?: boolean;
|
|
48
|
+
readonly includeDriversLicense?: boolean;
|
|
49
|
+
readonly includeRightToWork?: boolean;
|
|
50
|
+
readonly includeNationality?: boolean;
|
|
36
51
|
}
|
|
37
52
|
/**
|
|
38
53
|
* Validates a picked contact. Only lengths are checked: phone numbers and
|
|
@@ -43,6 +58,9 @@ declare const exportContactSchema: z.ZodObject<{
|
|
|
43
58
|
email: z.ZodOptional<z.ZodString>;
|
|
44
59
|
phone: z.ZodOptional<z.ZodString>;
|
|
45
60
|
address: z.ZodOptional<z.ZodString>;
|
|
61
|
+
driversLicense: z.ZodOptional<z.ZodString>;
|
|
62
|
+
rightToWork: z.ZodOptional<z.ZodString>;
|
|
63
|
+
nationality: z.ZodOptional<z.ZodString>;
|
|
46
64
|
}, z.core.$strict>;
|
|
47
65
|
/**
|
|
48
66
|
* The fields to put in an export: the included ones that are not blank, run
|
|
@@ -50,8 +68,8 @@ declare const exportContactSchema: z.ZodObject<{
|
|
|
50
68
|
*/
|
|
51
69
|
declare function pickExportContact(draft: ExportContactDraft): ExportContact;
|
|
52
70
|
/**
|
|
53
|
-
* Drops blank fields and tidies the rest.
|
|
54
|
-
* single spaces. An address keeps its line breaks but loses blank lines and the
|
|
71
|
+
* Drops blank fields and tidies the rest. Every field but the address becomes
|
|
72
|
+
* one line with single spaces. An address keeps its line breaks but loses blank lines and the
|
|
55
73
|
* spaces around each line. Each field is cut at its {@link EXPORT_CONTACT_LIMITS}
|
|
56
74
|
* length. The emitters call this too, so a caller that skips
|
|
57
75
|
* {@link pickExportContact} and the schema gets the same bounded output.
|
|
@@ -94,7 +112,10 @@ interface JsonResumeOptions {
|
|
|
94
112
|
/**
|
|
95
113
|
* Email, phone and postal address to print. Sifa holds none of these; the
|
|
96
114
|
* owner types them on the export page. Without it the resume has no
|
|
97
|
-
* `email`, `phone` or `location.address`.
|
|
115
|
+
* `email`, `phone` or `location.address`. Personal details in the same object
|
|
116
|
+
* (driver's license, right to work, nationality) are left out: JSON Resume
|
|
117
|
+
* has no field for them, and strict consumers such as altacv refuse a file
|
|
118
|
+
* with an unknown key.
|
|
98
119
|
*/
|
|
99
120
|
readonly contact?: ExportContact;
|
|
100
121
|
}
|
|
@@ -366,14 +387,17 @@ interface FunderCvPerson {
|
|
|
366
387
|
readonly website?: string;
|
|
367
388
|
}
|
|
368
389
|
/**
|
|
369
|
-
* The ERC person, with the contact details the owner typed for
|
|
370
|
-
* The NIH Biographical Sketch has no
|
|
390
|
+
* The ERC person, with the contact and personal details the owner typed for
|
|
391
|
+
* this export. The NIH Biographical Sketch has no such block, so only ERC carries them.
|
|
371
392
|
*/
|
|
372
393
|
interface ErcFunderCvPerson extends FunderCvPerson {
|
|
373
394
|
readonly email?: string;
|
|
374
395
|
readonly phone?: string;
|
|
375
396
|
/** Lines are separated by `\n`. */
|
|
376
397
|
readonly address?: string;
|
|
398
|
+
readonly driversLicense?: string;
|
|
399
|
+
readonly rightToWork?: string;
|
|
400
|
+
readonly nationality?: string;
|
|
377
401
|
}
|
|
378
402
|
interface FunderCvOptions {
|
|
379
403
|
/**
|
package/dist/resume/index.d.ts
CHANGED
|
@@ -3,7 +3,8 @@ import { L as LocationValue, d as ProfilePosition, f as ProfileEducation, P as P
|
|
|
3
3
|
import '../verification-providers-DeyGqaoC.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Private contact details for exports: email, phone
|
|
6
|
+
* Private contact and personal details for exports: email, phone, postal
|
|
7
|
+
* address, driver's license, right to work and nationality.
|
|
7
8
|
*
|
|
8
9
|
* Sifa never stores these and never writes them to the owner's PDS, where they
|
|
9
10
|
* would be public. The owner types them on the export page, the browser keeps
|
|
@@ -17,6 +18,9 @@ declare const EXPORT_CONTACT_LIMITS: {
|
|
|
17
18
|
readonly email: 254;
|
|
18
19
|
readonly phone: 40;
|
|
19
20
|
readonly address: 500;
|
|
21
|
+
readonly driversLicense: 60;
|
|
22
|
+
readonly rightToWork: 200;
|
|
23
|
+
readonly nationality: 100;
|
|
20
24
|
};
|
|
21
25
|
/** The contact details that go into an export. Absent keys are left out. */
|
|
22
26
|
interface ExportContact {
|
|
@@ -24,15 +28,26 @@ interface ExportContact {
|
|
|
24
28
|
readonly phone?: string;
|
|
25
29
|
/** Free text; lines are separated by `\n`. */
|
|
26
30
|
readonly address?: string;
|
|
31
|
+
/** License categories, e.g. "B, BE". */
|
|
32
|
+
readonly driversLicense?: string;
|
|
33
|
+
/** e.g. "EU citizen, no sponsorship needed". */
|
|
34
|
+
readonly rightToWork?: string;
|
|
35
|
+
readonly nationality?: string;
|
|
27
36
|
}
|
|
28
37
|
/** What the owner has typed, plus which fields they chose to include. */
|
|
29
38
|
interface ExportContactDraft {
|
|
30
39
|
readonly email?: string;
|
|
31
40
|
readonly phone?: string;
|
|
32
41
|
readonly address?: string;
|
|
42
|
+
readonly driversLicense?: string;
|
|
43
|
+
readonly rightToWork?: string;
|
|
44
|
+
readonly nationality?: string;
|
|
33
45
|
readonly includeEmail?: boolean;
|
|
34
46
|
readonly includePhone?: boolean;
|
|
35
47
|
readonly includeAddress?: boolean;
|
|
48
|
+
readonly includeDriversLicense?: boolean;
|
|
49
|
+
readonly includeRightToWork?: boolean;
|
|
50
|
+
readonly includeNationality?: boolean;
|
|
36
51
|
}
|
|
37
52
|
/**
|
|
38
53
|
* Validates a picked contact. Only lengths are checked: phone numbers and
|
|
@@ -43,6 +58,9 @@ declare const exportContactSchema: z.ZodObject<{
|
|
|
43
58
|
email: z.ZodOptional<z.ZodString>;
|
|
44
59
|
phone: z.ZodOptional<z.ZodString>;
|
|
45
60
|
address: z.ZodOptional<z.ZodString>;
|
|
61
|
+
driversLicense: z.ZodOptional<z.ZodString>;
|
|
62
|
+
rightToWork: z.ZodOptional<z.ZodString>;
|
|
63
|
+
nationality: z.ZodOptional<z.ZodString>;
|
|
46
64
|
}, z.core.$strict>;
|
|
47
65
|
/**
|
|
48
66
|
* The fields to put in an export: the included ones that are not blank, run
|
|
@@ -50,8 +68,8 @@ declare const exportContactSchema: z.ZodObject<{
|
|
|
50
68
|
*/
|
|
51
69
|
declare function pickExportContact(draft: ExportContactDraft): ExportContact;
|
|
52
70
|
/**
|
|
53
|
-
* Drops blank fields and tidies the rest.
|
|
54
|
-
* single spaces. An address keeps its line breaks but loses blank lines and the
|
|
71
|
+
* Drops blank fields and tidies the rest. Every field but the address becomes
|
|
72
|
+
* one line with single spaces. An address keeps its line breaks but loses blank lines and the
|
|
55
73
|
* spaces around each line. Each field is cut at its {@link EXPORT_CONTACT_LIMITS}
|
|
56
74
|
* length. The emitters call this too, so a caller that skips
|
|
57
75
|
* {@link pickExportContact} and the schema gets the same bounded output.
|
|
@@ -94,7 +112,10 @@ interface JsonResumeOptions {
|
|
|
94
112
|
/**
|
|
95
113
|
* Email, phone and postal address to print. Sifa holds none of these; the
|
|
96
114
|
* owner types them on the export page. Without it the resume has no
|
|
97
|
-
* `email`, `phone` or `location.address`.
|
|
115
|
+
* `email`, `phone` or `location.address`. Personal details in the same object
|
|
116
|
+
* (driver's license, right to work, nationality) are left out: JSON Resume
|
|
117
|
+
* has no field for them, and strict consumers such as altacv refuse a file
|
|
118
|
+
* with an unknown key.
|
|
98
119
|
*/
|
|
99
120
|
readonly contact?: ExportContact;
|
|
100
121
|
}
|
|
@@ -366,14 +387,17 @@ interface FunderCvPerson {
|
|
|
366
387
|
readonly website?: string;
|
|
367
388
|
}
|
|
368
389
|
/**
|
|
369
|
-
* The ERC person, with the contact details the owner typed for
|
|
370
|
-
* The NIH Biographical Sketch has no
|
|
390
|
+
* The ERC person, with the contact and personal details the owner typed for
|
|
391
|
+
* this export. The NIH Biographical Sketch has no such block, so only ERC carries them.
|
|
371
392
|
*/
|
|
372
393
|
interface ErcFunderCvPerson extends FunderCvPerson {
|
|
373
394
|
readonly email?: string;
|
|
374
395
|
readonly phone?: string;
|
|
375
396
|
/** Lines are separated by `\n`. */
|
|
376
397
|
readonly address?: string;
|
|
398
|
+
readonly driversLicense?: string;
|
|
399
|
+
readonly rightToWork?: string;
|
|
400
|
+
readonly nationality?: string;
|
|
377
401
|
}
|
|
378
402
|
interface FunderCvOptions {
|
|
379
403
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/resume/json-resume.ts"],"sourcesContent":["/**\n * JSON Resume v1.0.0 emitter for a Sifa profile.\n *\n * https://jsonresume.org/schema/\n *\n * One pure function, no fetching, no DOM, no framework -- the same shape as the\n * JSON-LD emitter next door, and for the same reason: sifa.id, page.sifa.id and\n * any future consumer should emit one document from one implementation.\n *\n * Why this format earns its place: it is the lingua franca of CV tooling, so\n * emitting it hands a user every JSON Resume theme, and the Typst CV templates\n * on Typst Universe, without Sifa writing a renderer for any of them.\n *\n * Fields Sifa holds that JSON Resume has no slot for -- endorsements,\n * confirmations, involvement, investments, presentations, courses -- are\n * dropped rather than smuggled into a nearby field. The loss is documented for\n * the user at the point of download.\n */\n\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport type {\n ExternalAccount,\n LanguageProficiency,\n LocationValue,\n ProfileCertification,\n ProfileEducation,\n ProfileHonor,\n ProfileLanguage,\n ProfilePosition,\n ProfileProject,\n ProfilePublication,\n ProfileSkill,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normaliseBaseUrl, profileUrl } from '../jsonld/url.js';\nimport { normalizeExportContact, type ExportContact } from './export-contact.js';\n\nexport type Sanitizer = (input: string) => string;\n\nexport interface JsonResumeOptions {\n /** Canonical origin for the profile URL. Defaults to `https://sifa.id`. */\n readonly baseUrl?: string;\n /** Applied to every user-authored string. Defaults to identity. */\n readonly sanitize?: Sanitizer;\n /**\n * Absolute URL of the profile this resume describes. Defaults to\n * `${baseUrl}/p/${handle}`. A personal site served from a custom domain is\n * not at that path.\n */\n readonly canonicalUrl?: string;\n /**\n * Email, phone and postal address to print. Sifa holds none of these; the\n * owner types them on the export page. Without it the resume has no\n * `email`, `phone` or `location.address`.\n */\n readonly contact?: ExportContact;\n}\n\n/**\n * The subset of `Profile` this emitter reads. Deliberately structural rather\n * than the full `Profile`, so a caller holding a partially hydrated profile --\n * or a test -- does not have to invent forty unrelated fields.\n */\nexport interface JsonResumeProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly avatar?: string;\n readonly headline?: string;\n readonly about?: string;\n readonly website?: string;\n readonly location?: LocationValue | null;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly skills?: ProfileSkill[];\n readonly certifications?: ProfileCertification[];\n readonly projects?: ProfileProject[];\n readonly publications?: ProfilePublication[];\n readonly volunteering?: ProfileVolunteering[];\n readonly honors?: ProfileHonor[];\n readonly languages?: ProfileLanguage[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface JsonResumeLocation {\n /** Street address; lines are separated by `\\n`, as the schema asks. */\n readonly address?: string;\n readonly city?: string;\n readonly region?: string;\n readonly countryCode?: string;\n readonly postalCode?: string;\n}\n\nexport interface JsonResumeProfileLink {\n readonly network: string;\n readonly username?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumeBasics {\n readonly name: string;\n readonly label?: string;\n readonly image?: string;\n readonly url?: string;\n readonly email?: string;\n readonly phone?: string;\n readonly summary?: string;\n readonly location?: JsonResumeLocation;\n readonly profiles?: JsonResumeProfileLink[];\n}\n\nexport interface JsonResumeWork {\n readonly name?: string;\n readonly position: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeVolunteer {\n /**\n * Omitted when the record carries no organization name at all. Emitting an\n * empty string would render as a blank organization line in a CV template.\n */\n readonly organization?: string;\n readonly position?: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeEducation {\n /** Omitted when the record carries no institution name. See volunteer. */\n readonly institution?: string;\n readonly area?: string;\n readonly studyType?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeAward {\n readonly title: string;\n readonly date?: string;\n readonly awarder?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeCertificate {\n readonly name: string;\n readonly date?: string;\n readonly issuer?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumePublication {\n readonly name: string;\n readonly publisher?: string;\n readonly releaseDate?: string;\n readonly url?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeSkill {\n readonly name: string;\n}\n\nexport interface JsonResumeLanguage {\n readonly language: string;\n readonly fluency?: string;\n}\n\nexport interface JsonResumeProject {\n readonly name: string;\n readonly description?: string;\n readonly url?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeMeta {\n readonly canonical: string;\n readonly version: string;\n readonly lastModified?: string;\n}\n\nexport interface JsonResume {\n readonly basics?: JsonResumeBasics;\n readonly work?: JsonResumeWork[];\n readonly volunteer?: JsonResumeVolunteer[];\n readonly education?: JsonResumeEducation[];\n readonly awards?: JsonResumeAward[];\n readonly certificates?: JsonResumeCertificate[];\n readonly publications?: JsonResumePublication[];\n readonly skills?: JsonResumeSkill[];\n readonly languages?: JsonResumeLanguage[];\n readonly projects?: JsonResumeProject[];\n readonly meta?: JsonResumeMeta;\n}\n\n/** Schema version this emitter targets, as JSON Resume writes it in `meta`. */\nconst SCHEMA_VERSION = 'v1.0.0';\n\n/**\n * A date JSON Resume will accept: `YYYY`, `YYYY-MM` or `YYYY-MM-DD`. Sifa\n * stores RFC 3339 for some fields, and a full timestamp pasted into a CV\n * template renders as `2020-03-04T00:00:00.000Z` on the page.\n */\nconst RESUME_DATE_RE = /^(\\d{4})(-\\d{2})?(-\\d{2})?/;\n\nexport function toResumeDate(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n if (!trimmed) return undefined;\n const match = RESUME_DATE_RE.exec(trimmed);\n return match ? match[0] : undefined;\n}\n\n/**\n * JSON Resume's `fluency` is free text, so these are display labels rather\n * than a mapped vocabulary. Kept in one place so export and a future import\n * agree on the spelling.\n */\nconst FLUENCY_LABELS: Record<LanguageProficiency, string> = {\n elementary: 'Elementary',\n limited_working: 'Limited working',\n professional_working: 'Professional working',\n full_professional: 'Full professional',\n native: 'Native or bilingual',\n};\n\nconst identity: Sanitizer = (input) => input;\n\n/** Drops keys whose value is undefined, so the document carries no empty slots. */\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\n/** Adds the owner's street address to the profile's location. */\nfunction withAddress(\n location: JsonResumeLocation | undefined,\n address: string | undefined,\n): JsonResumeLocation | undefined {\n return address ? { address, ...location } : location;\n}\n\n/** Omits the key entirely when the mapped list came out empty. */\nfunction section<K extends string, T>(key: K, items: T[]): Record<K, T[]> | undefined {\n return items.length > 0 ? ({ [key]: items } as Record<K, T[]>) : undefined;\n}\n\nfunction mapLocation(\n location: LocationValue | null | undefined,\n s: Sanitizer,\n): JsonResumeLocation | undefined {\n if (!location) return undefined;\n const mapped = compact({\n city: text(location.locality ?? location.city, s),\n region: text(location.region, s),\n countryCode: text(location.countryCode, s),\n postalCode: text(location.postalCode, s),\n });\n return Object.keys(mapped).length > 0 ? mapped : undefined;\n}\n\n/** Trimmed, sanitised, and undefined when there is nothing left. */\nfunction text(value: string | undefined, s: Sanitizer): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? s(trimmed) : undefined;\n}\n\n/**\n * The name to print for a linked organization. `entityName` is the AppView's\n * resolved current name and wins when present, matching how the profile itself\n * renders these.\n */\nfunction orgName(\n entityName: string | undefined,\n stored: string | undefined,\n s: Sanitizer,\n): string | undefined {\n return text(entityName, s) ?? text(stored, s);\n}\n\n/**\n * Build a JSON Resume v1.0.0 document from a Sifa profile.\n *\n * Hidden records are dropped. Keys are omitted rather than emitted empty, so a\n * profile with no publications produces a document with no `publications` key\n * instead of one a reader has to interpret.\n */\nexport function profileToJsonResume(\n profile: JsonResumeProfileInput,\n options: JsonResumeOptions = {},\n): JsonResume {\n const s = options.sanitize ?? identity;\n const baseUrl = normaliseBaseUrl(options.baseUrl);\n const canonical = options.canonicalUrl ?? profileUrl(baseUrl, profile.handle);\n const contact = normalizeExportContact(options.contact ?? {});\n\n const name =\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName, s) ??\n profile.handle;\n\n const profiles = filterHidden(profile.externalAccounts).map((account) =>\n compact<JsonResumeProfileLink>({\n network: s(account.platform),\n username: text(account.label, s),\n url: text(account.url, s),\n }),\n );\n\n const basics = compact<JsonResumeBasics>({\n name,\n label: text(profile.headline, s),\n image: text(profile.avatar, s),\n email: text(contact.email, s),\n phone: text(contact.phone, s),\n url: text(profile.website, s) ?? canonical,\n summary: text(profile.about, s),\n location: withAddress(mapLocation(profile.location, s), text(contact.address, s)),\n ...section('profiles', profiles),\n });\n\n const work = filterHidden(profile.positions).map((position) =>\n compact<JsonResumeWork>({\n name: orgName(position.entityName, position.company, s),\n position: s(position.title),\n summary: text(position.description, s),\n startDate: toResumeDate(position.startedAt),\n endDate: toResumeDate(position.endedAt),\n }),\n );\n\n const volunteer = filterHidden(profile.volunteering).map((entry) =>\n compact<JsonResumeVolunteer>({\n organization: orgName(entry.entityName, entry.organization, s),\n position: text(entry.role, s),\n summary: text(entry.description, s),\n startDate: toResumeDate(entry.startDate),\n endDate: toResumeDate(entry.endDate),\n }),\n );\n\n const education = filterHidden(profile.education).map((entry) =>\n compact<JsonResumeEducation>({\n institution: orgName(entry.entityName, entry.institution, s),\n area: text(entry.fieldOfStudy, s),\n studyType: text(entry.degree, s),\n startDate: toResumeDate(entry.startedAt),\n endDate: toResumeDate(entry.endedAt),\n }),\n );\n\n const awards = filterHidden(profile.honors).map((honor) =>\n compact<JsonResumeAward>({\n title: s(honor.title),\n date: toResumeDate(honor.date),\n awarder: orgName(honor.entityName, honor.issuer, s),\n summary: text(honor.description, s),\n }),\n );\n\n const certificates = filterHidden(profile.certifications).map((cert) =>\n compact<JsonResumeCertificate>({\n name: s(cert.name),\n date: toResumeDate(cert.issueDate),\n // `issuingOrg` is the deprecated read-view alias for `authority`; records\n // indexed before the rename carry only the alias.\n issuer: orgName(cert.entityName, cert.authority ?? cert.issuingOrg, s),\n url: text(cert.credentialUrl, s),\n }),\n );\n\n const publications = filterHidden(profile.publications).map((publication) =>\n compact<JsonResumePublication>({\n name: s(publication.title),\n publisher: text(publication.publisher, s),\n releaseDate: toResumeDate(publication.date),\n url: text(publication.url, s),\n summary: text(publication.description, s),\n }),\n );\n\n // One entry per skill rather than JSON Resume's grouped shape: Sifa stores one\n // record per skill, and keeping the two symmetric is what makes a round-trip\n // through a third-party editor safe. Category and endorsement counts have no\n // JSON Resume slot and are dropped. Skill records carry no `hidden` flag, so\n // there is nothing to filter here.\n const skills = (profile.skills ?? []).map((skill) => ({ name: s(skill.name) }));\n\n const languages = filterHidden(profile.languages).map((language) =>\n compact<JsonResumeLanguage>({\n language: s(language.language),\n fluency: language.proficiency ? FLUENCY_LABELS[language.proficiency] : undefined,\n }),\n );\n\n const projects = filterHidden(profile.projects).map((project) =>\n compact<JsonResumeProject>({\n name: s(project.name),\n description: text(project.description, s),\n url: text(project.url, s),\n startDate: toResumeDate(project.startDate),\n endDate: toResumeDate(project.endDate),\n }),\n );\n\n return {\n basics,\n ...section('work', work),\n ...section('volunteer', volunteer),\n ...section('education', education),\n ...section('awards', awards),\n ...section('certificates', certificates),\n ...section('publications', publications),\n ...section('skills', skills),\n ...section('languages', languages),\n ...section('projects', projects),\n meta: { canonical, version: SCHEMA_VERSION },\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/resume/json-resume.ts"],"sourcesContent":["/**\n * JSON Resume v1.0.0 emitter for a Sifa profile.\n *\n * https://jsonresume.org/schema/\n *\n * One pure function, no fetching, no DOM, no framework -- the same shape as the\n * JSON-LD emitter next door, and for the same reason: sifa.id, page.sifa.id and\n * any future consumer should emit one document from one implementation.\n *\n * Why this format earns its place: it is the lingua franca of CV tooling, so\n * emitting it hands a user every JSON Resume theme, and the Typst CV templates\n * on Typst Universe, without Sifa writing a renderer for any of them.\n *\n * Fields Sifa holds that JSON Resume has no slot for -- endorsements,\n * confirmations, involvement, investments, presentations, courses -- are\n * dropped rather than smuggled into a nearby field. The loss is documented for\n * the user at the point of download.\n */\n\nimport { formatStructuredName } from '../format/pds-utils.js';\nimport { filterHidden } from '../profile/section-model.js';\nimport type {\n ExternalAccount,\n LanguageProficiency,\n LocationValue,\n ProfileCertification,\n ProfileEducation,\n ProfileHonor,\n ProfileLanguage,\n ProfilePosition,\n ProfileProject,\n ProfilePublication,\n ProfileSkill,\n ProfileVolunteering,\n} from '../types/index.js';\nimport { normaliseBaseUrl, profileUrl } from '../jsonld/url.js';\nimport { normalizeExportContact, type ExportContact } from './export-contact.js';\n\nexport type Sanitizer = (input: string) => string;\n\nexport interface JsonResumeOptions {\n /** Canonical origin for the profile URL. Defaults to `https://sifa.id`. */\n readonly baseUrl?: string;\n /** Applied to every user-authored string. Defaults to identity. */\n readonly sanitize?: Sanitizer;\n /**\n * Absolute URL of the profile this resume describes. Defaults to\n * `${baseUrl}/p/${handle}`. A personal site served from a custom domain is\n * not at that path.\n */\n readonly canonicalUrl?: string;\n /**\n * Email, phone and postal address to print. Sifa holds none of these; the\n * owner types them on the export page. Without it the resume has no\n * `email`, `phone` or `location.address`. Personal details in the same object\n * (driver's license, right to work, nationality) are left out: JSON Resume\n * has no field for them, and strict consumers such as altacv refuse a file\n * with an unknown key.\n */\n readonly contact?: ExportContact;\n}\n\n/**\n * The subset of `Profile` this emitter reads. Deliberately structural rather\n * than the full `Profile`, so a caller holding a partially hydrated profile --\n * or a test -- does not have to invent forty unrelated fields.\n */\nexport interface JsonResumeProfileInput {\n readonly handle: string;\n readonly displayName?: string;\n readonly givenName?: string;\n readonly familyName?: string;\n readonly avatar?: string;\n readonly headline?: string;\n readonly about?: string;\n readonly website?: string;\n readonly location?: LocationValue | null;\n readonly positions?: ProfilePosition[];\n readonly education?: ProfileEducation[];\n readonly skills?: ProfileSkill[];\n readonly certifications?: ProfileCertification[];\n readonly projects?: ProfileProject[];\n readonly publications?: ProfilePublication[];\n readonly volunteering?: ProfileVolunteering[];\n readonly honors?: ProfileHonor[];\n readonly languages?: ProfileLanguage[];\n readonly externalAccounts?: ExternalAccount[];\n}\n\nexport interface JsonResumeLocation {\n /** Street address; lines are separated by `\\n`, as the schema asks. */\n readonly address?: string;\n readonly city?: string;\n readonly region?: string;\n readonly countryCode?: string;\n readonly postalCode?: string;\n}\n\nexport interface JsonResumeProfileLink {\n readonly network: string;\n readonly username?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumeBasics {\n readonly name: string;\n readonly label?: string;\n readonly image?: string;\n readonly url?: string;\n readonly email?: string;\n readonly phone?: string;\n readonly summary?: string;\n readonly location?: JsonResumeLocation;\n readonly profiles?: JsonResumeProfileLink[];\n}\n\nexport interface JsonResumeWork {\n readonly name?: string;\n readonly position: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeVolunteer {\n /**\n * Omitted when the record carries no organization name at all. Emitting an\n * empty string would render as a blank organization line in a CV template.\n */\n readonly organization?: string;\n readonly position?: string;\n readonly summary?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeEducation {\n /** Omitted when the record carries no institution name. See volunteer. */\n readonly institution?: string;\n readonly area?: string;\n readonly studyType?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeAward {\n readonly title: string;\n readonly date?: string;\n readonly awarder?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeCertificate {\n readonly name: string;\n readonly date?: string;\n readonly issuer?: string;\n readonly url?: string;\n}\n\nexport interface JsonResumePublication {\n readonly name: string;\n readonly publisher?: string;\n readonly releaseDate?: string;\n readonly url?: string;\n readonly summary?: string;\n}\n\nexport interface JsonResumeSkill {\n readonly name: string;\n}\n\nexport interface JsonResumeLanguage {\n readonly language: string;\n readonly fluency?: string;\n}\n\nexport interface JsonResumeProject {\n readonly name: string;\n readonly description?: string;\n readonly url?: string;\n readonly startDate?: string;\n readonly endDate?: string;\n}\n\nexport interface JsonResumeMeta {\n readonly canonical: string;\n readonly version: string;\n readonly lastModified?: string;\n}\n\nexport interface JsonResume {\n readonly basics?: JsonResumeBasics;\n readonly work?: JsonResumeWork[];\n readonly volunteer?: JsonResumeVolunteer[];\n readonly education?: JsonResumeEducation[];\n readonly awards?: JsonResumeAward[];\n readonly certificates?: JsonResumeCertificate[];\n readonly publications?: JsonResumePublication[];\n readonly skills?: JsonResumeSkill[];\n readonly languages?: JsonResumeLanguage[];\n readonly projects?: JsonResumeProject[];\n readonly meta?: JsonResumeMeta;\n}\n\n/** Schema version this emitter targets, as JSON Resume writes it in `meta`. */\nconst SCHEMA_VERSION = 'v1.0.0';\n\n/**\n * A date JSON Resume will accept: `YYYY`, `YYYY-MM` or `YYYY-MM-DD`. Sifa\n * stores RFC 3339 for some fields, and a full timestamp pasted into a CV\n * template renders as `2020-03-04T00:00:00.000Z` on the page.\n */\nconst RESUME_DATE_RE = /^(\\d{4})(-\\d{2})?(-\\d{2})?/;\n\nexport function toResumeDate(value: string | undefined): string | undefined {\n const trimmed = value?.trim();\n if (!trimmed) return undefined;\n const match = RESUME_DATE_RE.exec(trimmed);\n return match ? match[0] : undefined;\n}\n\n/**\n * JSON Resume's `fluency` is free text, so these are display labels rather\n * than a mapped vocabulary. Kept in one place so export and a future import\n * agree on the spelling.\n */\nconst FLUENCY_LABELS: Record<LanguageProficiency, string> = {\n elementary: 'Elementary',\n limited_working: 'Limited working',\n professional_working: 'Professional working',\n full_professional: 'Full professional',\n native: 'Native or bilingual',\n};\n\nconst identity: Sanitizer = (input) => input;\n\n/** Drops keys whose value is undefined, so the document carries no empty slots. */\nfunction compact<T extends object>(value: T): T {\n return Object.fromEntries(Object.entries(value).filter(([, v]) => v !== undefined)) as T;\n}\n\n/** Adds the owner's street address to the profile's location. */\nfunction withAddress(\n location: JsonResumeLocation | undefined,\n address: string | undefined,\n): JsonResumeLocation | undefined {\n return address ? { address, ...location } : location;\n}\n\n/** Omits the key entirely when the mapped list came out empty. */\nfunction section<K extends string, T>(key: K, items: T[]): Record<K, T[]> | undefined {\n return items.length > 0 ? ({ [key]: items } as Record<K, T[]>) : undefined;\n}\n\nfunction mapLocation(\n location: LocationValue | null | undefined,\n s: Sanitizer,\n): JsonResumeLocation | undefined {\n if (!location) return undefined;\n const mapped = compact({\n city: text(location.locality ?? location.city, s),\n region: text(location.region, s),\n countryCode: text(location.countryCode, s),\n postalCode: text(location.postalCode, s),\n });\n return Object.keys(mapped).length > 0 ? mapped : undefined;\n}\n\n/** Trimmed, sanitised, and undefined when there is nothing left. */\nfunction text(value: string | undefined, s: Sanitizer): string | undefined {\n const trimmed = value?.trim();\n return trimmed ? s(trimmed) : undefined;\n}\n\n/**\n * The name to print for a linked organization. `entityName` is the AppView's\n * resolved current name and wins when present, matching how the profile itself\n * renders these.\n */\nfunction orgName(\n entityName: string | undefined,\n stored: string | undefined,\n s: Sanitizer,\n): string | undefined {\n return text(entityName, s) ?? text(stored, s);\n}\n\n/**\n * Build a JSON Resume v1.0.0 document from a Sifa profile.\n *\n * Hidden records are dropped. Keys are omitted rather than emitted empty, so a\n * profile with no publications produces a document with no `publications` key\n * instead of one a reader has to interpret.\n */\nexport function profileToJsonResume(\n profile: JsonResumeProfileInput,\n options: JsonResumeOptions = {},\n): JsonResume {\n const s = options.sanitize ?? identity;\n const baseUrl = normaliseBaseUrl(options.baseUrl);\n const canonical = options.canonicalUrl ?? profileUrl(baseUrl, profile.handle);\n const contact = normalizeExportContact(options.contact ?? {});\n\n const name =\n formatStructuredName(profile.givenName, profile.familyName) ??\n text(profile.displayName, s) ??\n profile.handle;\n\n const profiles = filterHidden(profile.externalAccounts).map((account) =>\n compact<JsonResumeProfileLink>({\n network: s(account.platform),\n username: text(account.label, s),\n url: text(account.url, s),\n }),\n );\n\n const basics = compact<JsonResumeBasics>({\n name,\n label: text(profile.headline, s),\n image: text(profile.avatar, s),\n email: text(contact.email, s),\n phone: text(contact.phone, s),\n url: text(profile.website, s) ?? canonical,\n summary: text(profile.about, s),\n location: withAddress(mapLocation(profile.location, s), text(contact.address, s)),\n ...section('profiles', profiles),\n });\n\n const work = filterHidden(profile.positions).map((position) =>\n compact<JsonResumeWork>({\n name: orgName(position.entityName, position.company, s),\n position: s(position.title),\n summary: text(position.description, s),\n startDate: toResumeDate(position.startedAt),\n endDate: toResumeDate(position.endedAt),\n }),\n );\n\n const volunteer = filterHidden(profile.volunteering).map((entry) =>\n compact<JsonResumeVolunteer>({\n organization: orgName(entry.entityName, entry.organization, s),\n position: text(entry.role, s),\n summary: text(entry.description, s),\n startDate: toResumeDate(entry.startDate),\n endDate: toResumeDate(entry.endDate),\n }),\n );\n\n const education = filterHidden(profile.education).map((entry) =>\n compact<JsonResumeEducation>({\n institution: orgName(entry.entityName, entry.institution, s),\n area: text(entry.fieldOfStudy, s),\n studyType: text(entry.degree, s),\n startDate: toResumeDate(entry.startedAt),\n endDate: toResumeDate(entry.endedAt),\n }),\n );\n\n const awards = filterHidden(profile.honors).map((honor) =>\n compact<JsonResumeAward>({\n title: s(honor.title),\n date: toResumeDate(honor.date),\n awarder: orgName(honor.entityName, honor.issuer, s),\n summary: text(honor.description, s),\n }),\n );\n\n const certificates = filterHidden(profile.certifications).map((cert) =>\n compact<JsonResumeCertificate>({\n name: s(cert.name),\n date: toResumeDate(cert.issueDate),\n // `issuingOrg` is the deprecated read-view alias for `authority`; records\n // indexed before the rename carry only the alias.\n issuer: orgName(cert.entityName, cert.authority ?? cert.issuingOrg, s),\n url: text(cert.credentialUrl, s),\n }),\n );\n\n const publications = filterHidden(profile.publications).map((publication) =>\n compact<JsonResumePublication>({\n name: s(publication.title),\n publisher: text(publication.publisher, s),\n releaseDate: toResumeDate(publication.date),\n url: text(publication.url, s),\n summary: text(publication.description, s),\n }),\n );\n\n // One entry per skill rather than JSON Resume's grouped shape: Sifa stores one\n // record per skill, and keeping the two symmetric is what makes a round-trip\n // through a third-party editor safe. Category and endorsement counts have no\n // JSON Resume slot and are dropped. Skill records carry no `hidden` flag, so\n // there is nothing to filter here.\n const skills = (profile.skills ?? []).map((skill) => ({ name: s(skill.name) }));\n\n const languages = filterHidden(profile.languages).map((language) =>\n compact<JsonResumeLanguage>({\n language: s(language.language),\n fluency: language.proficiency ? FLUENCY_LABELS[language.proficiency] : undefined,\n }),\n );\n\n const projects = filterHidden(profile.projects).map((project) =>\n compact<JsonResumeProject>({\n name: s(project.name),\n description: text(project.description, s),\n url: text(project.url, s),\n startDate: toResumeDate(project.startDate),\n endDate: toResumeDate(project.endDate),\n }),\n );\n\n return {\n basics,\n ...section('work', work),\n ...section('volunteer', volunteer),\n ...section('education', education),\n ...section('awards', awards),\n ...section('certificates', certificates),\n ...section('publications', publications),\n ...section('skills', skills),\n ...section('languages', languages),\n ...section('projects', projects),\n meta: { canonical, version: SCHEMA_VERSION },\n };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
CHANGED