@singi-labs/sifa-sdk 0.18.5 → 0.18.7
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/{network-map-Ds3OR7eJ.d.cts → network-map-3jIMzuUY.d.cts} +1 -1
- package/dist/{network-map-DZjcO7tM.d.ts → network-map-CqgdF6Gr.d.ts} +1 -1
- package/dist/{org-DmtI6C4A.d.cts → org-ANztvuTe.d.cts} +56 -3
- package/dist/{org-h8nhEeAJ.d.ts → org-DS183N90.d.ts} +56 -3
- package/dist/{org-settings-CZ_tmAYl.d.cts → org-settings-B8e01iZx.d.cts} +1 -1
- package/dist/{org-settings-CZ_tmAYl.d.ts → org-settings-B8e01iZx.d.ts} +1 -1
- package/dist/query/fetchers/index.cjs +29 -1
- package/dist/query/fetchers/index.cjs.map +1 -1
- package/dist/query/fetchers/index.d.cts +4 -4
- package/dist/query/fetchers/index.d.ts +4 -4
- package/dist/query/fetchers/index.js +27 -2
- package/dist/query/fetchers/index.js.map +1 -1
- package/dist/query/hooks/index.cjs +61 -1
- package/dist/query/hooks/index.cjs.map +1 -1
- package/dist/query/hooks/index.d.cts +4 -4
- package/dist/query/hooks/index.d.ts +4 -4
- package/dist/query/hooks/index.js +59 -2
- package/dist/query/hooks/index.js.map +1 -1
- package/dist/query/index.cjs +62 -1
- package/dist/query/index.cjs.map +1 -1
- package/dist/query/index.d.cts +6 -6
- package/dist/query/index.d.ts +6 -6
- package/dist/query/index.js +57 -2
- package/dist/query/index.js.map +1 -1
- package/dist/resume/index.cjs +4 -4
- package/dist/resume/index.cjs.map +1 -1
- package/dist/resume/index.d.cts +8 -3
- package/dist/resume/index.d.ts +8 -3
- package/dist/resume/index.js +4 -4
- package/dist/resume/index.js.map +1 -1
- package/dist/schemas/write/index.d.cts +1 -1
- package/dist/schemas/write/index.d.ts +1 -1
- package/dist/{use-org-notification-emails-Cz4cuTyg.d.ts → use-org-notification-emails-CNDrIUcS.d.ts} +16 -3
- package/dist/{use-org-notification-emails-CkCQJhy8.d.cts → use-org-notification-emails-D4JRlCX2.d.cts} +16 -3
- package/package.json +1 -1
package/dist/resume/index.cjs
CHANGED
|
@@ -81,13 +81,13 @@ function profileToJsonResume(profile, options = {}) {
|
|
|
81
81
|
(account) => compact({
|
|
82
82
|
network: s(account.platform),
|
|
83
83
|
username: text(account.label, s),
|
|
84
|
-
url: account.url
|
|
84
|
+
url: text(account.url, s)
|
|
85
85
|
})
|
|
86
86
|
);
|
|
87
87
|
const basics = compact({
|
|
88
88
|
name,
|
|
89
89
|
label: text(profile.headline, s),
|
|
90
|
-
image: profile.avatar,
|
|
90
|
+
image: text(profile.avatar, s),
|
|
91
91
|
url: text(profile.website, s) ?? canonical,
|
|
92
92
|
summary: text(profile.about, s),
|
|
93
93
|
location: mapLocation(profile.location, s),
|
|
@@ -104,7 +104,7 @@ function profileToJsonResume(profile, options = {}) {
|
|
|
104
104
|
);
|
|
105
105
|
const volunteer = filterHidden(profile.volunteering).map(
|
|
106
106
|
(entry) => compact({
|
|
107
|
-
organization: orgName(entry.entityName, entry.organization, s)
|
|
107
|
+
organization: orgName(entry.entityName, entry.organization, s),
|
|
108
108
|
position: text(entry.role, s),
|
|
109
109
|
summary: text(entry.description, s),
|
|
110
110
|
startDate: toResumeDate(entry.startDate),
|
|
@@ -113,7 +113,7 @@ function profileToJsonResume(profile, options = {}) {
|
|
|
113
113
|
);
|
|
114
114
|
const education = filterHidden(profile.education).map(
|
|
115
115
|
(entry) => compact({
|
|
116
|
-
institution: orgName(entry.entityName, entry.institution, s)
|
|
116
|
+
institution: orgName(entry.entityName, entry.institution, s),
|
|
117
117
|
area: text(entry.fieldOfStudy, s),
|
|
118
118
|
studyType: text(entry.degree, s),
|
|
119
119
|
startDate: toResumeDate(entry.startedAt),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/format/pds-utils.ts","../../src/profile/section-model.ts","../../src/jsonld/url.ts","../../src/resume/json-resume.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["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, skills, projects, talks,\n * publications), then the formal record (credentials, education, courses), then\n * the 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: 'skills', labelKey: 'skills', 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 {\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/** Whether a section has content for a given profile (used to hide empty nav entries for visitors). */\nexport function isSectionPopulated(profile: Profile, id: string): boolean {\n switch (id) {\n case 'about':\n return Boolean(profile.about && profile.headline);\n case 'career':\n return Boolean(profile.positions?.length);\n case 'education':\n return Boolean(profile.education?.length);\n case 'courses':\n return Boolean(profile.courses?.length);\n case 'skills':\n return Boolean(profile.skills?.length);\n case 'projects':\n return Boolean(profile.projects?.length);\n case 'credentials':\n return Boolean(profile.certifications?.length);\n case 'publications':\n return Boolean(profile.publications?.length);\n case 'presentations':\n return (\n Boolean(profile.presentations?.length) || Boolean(profile.presentationDeliveries?.length)\n );\n case 'involvement':\n return Boolean(profile.involvement?.length);\n case 'investments':\n return Boolean(profile.investments?.length);\n case 'awards':\n return Boolean(profile.honors?.length);\n case 'languages':\n return Boolean(profile.languages?.length);\n case 'other-profiles':\n return Boolean(profile.externalAccounts?.length);\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';\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\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 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 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 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 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/** 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\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: account.url,\n }),\n );\n\n const basics = compact<JsonResumeBasics>({\n name,\n label: text(profile.headline, s),\n image: profile.avatar,\n url: text(profile.website, s) ?? canonical,\n summary: text(profile.about, s),\n location: mapLocation(profile.location, 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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/format/pds-utils.ts","../../src/profile/section-model.ts","../../src/jsonld/url.ts","../../src/resume/json-resume.ts"],"names":[],"mappings":"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","file":"index.cjs","sourcesContent":["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, skills, projects, talks,\n * publications), then the formal record (credentials, education, courses), then\n * the 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: 'skills', labelKey: 'skills', 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 {\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/** Whether a section has content for a given profile (used to hide empty nav entries for visitors). */\nexport function isSectionPopulated(profile: Profile, id: string): boolean {\n switch (id) {\n case 'about':\n return Boolean(profile.about && profile.headline);\n case 'career':\n return Boolean(profile.positions?.length);\n case 'education':\n return Boolean(profile.education?.length);\n case 'courses':\n return Boolean(profile.courses?.length);\n case 'skills':\n return Boolean(profile.skills?.length);\n case 'projects':\n return Boolean(profile.projects?.length);\n case 'credentials':\n return Boolean(profile.certifications?.length);\n case 'publications':\n return Boolean(profile.publications?.length);\n case 'presentations':\n return (\n Boolean(profile.presentations?.length) || Boolean(profile.presentationDeliveries?.length)\n );\n case 'involvement':\n return Boolean(profile.involvement?.length);\n case 'investments':\n return Boolean(profile.investments?.length);\n case 'awards':\n return Boolean(profile.honors?.length);\n case 'languages':\n return Boolean(profile.languages?.length);\n case 'other-profiles':\n return Boolean(profile.externalAccounts?.length);\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';\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\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 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 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/** 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\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 url: text(profile.website, s) ?? canonical,\n summary: text(profile.about, s),\n location: mapLocation(profile.location, 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"]}
|
package/dist/resume/index.d.cts
CHANGED
|
@@ -67,7 +67,7 @@ interface JsonResumeLocation {
|
|
|
67
67
|
interface JsonResumeProfileLink {
|
|
68
68
|
readonly network: string;
|
|
69
69
|
readonly username?: string;
|
|
70
|
-
readonly url
|
|
70
|
+
readonly url?: string;
|
|
71
71
|
}
|
|
72
72
|
interface JsonResumeBasics {
|
|
73
73
|
readonly name: string;
|
|
@@ -86,14 +86,19 @@ interface JsonResumeWork {
|
|
|
86
86
|
readonly endDate?: string;
|
|
87
87
|
}
|
|
88
88
|
interface JsonResumeVolunteer {
|
|
89
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Omitted when the record carries no organization name at all. Emitting an
|
|
91
|
+
* empty string would render as a blank organization line in a CV template.
|
|
92
|
+
*/
|
|
93
|
+
readonly organization?: string;
|
|
90
94
|
readonly position?: string;
|
|
91
95
|
readonly summary?: string;
|
|
92
96
|
readonly startDate?: string;
|
|
93
97
|
readonly endDate?: string;
|
|
94
98
|
}
|
|
95
99
|
interface JsonResumeEducation {
|
|
96
|
-
|
|
100
|
+
/** Omitted when the record carries no institution name. See volunteer. */
|
|
101
|
+
readonly institution?: string;
|
|
97
102
|
readonly area?: string;
|
|
98
103
|
readonly studyType?: string;
|
|
99
104
|
readonly startDate?: string;
|
package/dist/resume/index.d.ts
CHANGED
|
@@ -67,7 +67,7 @@ interface JsonResumeLocation {
|
|
|
67
67
|
interface JsonResumeProfileLink {
|
|
68
68
|
readonly network: string;
|
|
69
69
|
readonly username?: string;
|
|
70
|
-
readonly url
|
|
70
|
+
readonly url?: string;
|
|
71
71
|
}
|
|
72
72
|
interface JsonResumeBasics {
|
|
73
73
|
readonly name: string;
|
|
@@ -86,14 +86,19 @@ interface JsonResumeWork {
|
|
|
86
86
|
readonly endDate?: string;
|
|
87
87
|
}
|
|
88
88
|
interface JsonResumeVolunteer {
|
|
89
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Omitted when the record carries no organization name at all. Emitting an
|
|
91
|
+
* empty string would render as a blank organization line in a CV template.
|
|
92
|
+
*/
|
|
93
|
+
readonly organization?: string;
|
|
90
94
|
readonly position?: string;
|
|
91
95
|
readonly summary?: string;
|
|
92
96
|
readonly startDate?: string;
|
|
93
97
|
readonly endDate?: string;
|
|
94
98
|
}
|
|
95
99
|
interface JsonResumeEducation {
|
|
96
|
-
|
|
100
|
+
/** Omitted when the record carries no institution name. See volunteer. */
|
|
101
|
+
readonly institution?: string;
|
|
97
102
|
readonly area?: string;
|
|
98
103
|
readonly studyType?: string;
|
|
99
104
|
readonly startDate?: string;
|
package/dist/resume/index.js
CHANGED
|
@@ -79,13 +79,13 @@ function profileToJsonResume(profile, options = {}) {
|
|
|
79
79
|
(account) => compact({
|
|
80
80
|
network: s(account.platform),
|
|
81
81
|
username: text(account.label, s),
|
|
82
|
-
url: account.url
|
|
82
|
+
url: text(account.url, s)
|
|
83
83
|
})
|
|
84
84
|
);
|
|
85
85
|
const basics = compact({
|
|
86
86
|
name,
|
|
87
87
|
label: text(profile.headline, s),
|
|
88
|
-
image: profile.avatar,
|
|
88
|
+
image: text(profile.avatar, s),
|
|
89
89
|
url: text(profile.website, s) ?? canonical,
|
|
90
90
|
summary: text(profile.about, s),
|
|
91
91
|
location: mapLocation(profile.location, s),
|
|
@@ -102,7 +102,7 @@ function profileToJsonResume(profile, options = {}) {
|
|
|
102
102
|
);
|
|
103
103
|
const volunteer = filterHidden(profile.volunteering).map(
|
|
104
104
|
(entry) => compact({
|
|
105
|
-
organization: orgName(entry.entityName, entry.organization, s)
|
|
105
|
+
organization: orgName(entry.entityName, entry.organization, s),
|
|
106
106
|
position: text(entry.role, s),
|
|
107
107
|
summary: text(entry.description, s),
|
|
108
108
|
startDate: toResumeDate(entry.startDate),
|
|
@@ -111,7 +111,7 @@ function profileToJsonResume(profile, options = {}) {
|
|
|
111
111
|
);
|
|
112
112
|
const education = filterHidden(profile.education).map(
|
|
113
113
|
(entry) => compact({
|
|
114
|
-
institution: orgName(entry.entityName, entry.institution, s)
|
|
114
|
+
institution: orgName(entry.entityName, entry.institution, s),
|
|
115
115
|
area: text(entry.fieldOfStudy, s),
|
|
116
116
|
studyType: text(entry.degree, s),
|
|
117
117
|
startDate: toResumeDate(entry.startedAt),
|
package/dist/resume/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/format/pds-utils.ts","../../src/profile/section-model.ts","../../src/jsonld/url.ts","../../src/resume/json-resume.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["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, skills, projects, talks,\n * publications), then the formal record (credentials, education, courses), then\n * the 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: 'skills', labelKey: 'skills', 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 {\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/** Whether a section has content for a given profile (used to hide empty nav entries for visitors). */\nexport function isSectionPopulated(profile: Profile, id: string): boolean {\n switch (id) {\n case 'about':\n return Boolean(profile.about && profile.headline);\n case 'career':\n return Boolean(profile.positions?.length);\n case 'education':\n return Boolean(profile.education?.length);\n case 'courses':\n return Boolean(profile.courses?.length);\n case 'skills':\n return Boolean(profile.skills?.length);\n case 'projects':\n return Boolean(profile.projects?.length);\n case 'credentials':\n return Boolean(profile.certifications?.length);\n case 'publications':\n return Boolean(profile.publications?.length);\n case 'presentations':\n return (\n Boolean(profile.presentations?.length) || Boolean(profile.presentationDeliveries?.length)\n );\n case 'involvement':\n return Boolean(profile.involvement?.length);\n case 'investments':\n return Boolean(profile.investments?.length);\n case 'awards':\n return Boolean(profile.honors?.length);\n case 'languages':\n return Boolean(profile.languages?.length);\n case 'other-profiles':\n return Boolean(profile.externalAccounts?.length);\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';\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\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 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 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 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 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/** 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\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: account.url,\n }),\n );\n\n const basics = compact<JsonResumeBasics>({\n name,\n label: text(profile.headline, s),\n image: profile.avatar,\n url: text(profile.website, s) ?? canonical,\n summary: text(profile.about, s),\n location: mapLocation(profile.location, 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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/format/pds-utils.ts","../../src/profile/section-model.ts","../../src/jsonld/url.ts","../../src/resume/json-resume.ts"],"names":[],"mappings":";AAUA,IAAM,aAAA,GAAoD;AAAA,EACxD,EAAE,MAAA,EAAQ,cAAA,EAAgB,IAAA,EAAM,SAAA,EAAU;AAAA,EAC1C,EAAE,MAAA,EAAQ,eAAA,EAAiB,IAAA,EAAM,UAAA,EAAW;AAAA,EAC5C,EAAE,MAAA,EAAQ,iBAAA,EAAmB,IAAA,EAAM,SAAA,EAAU;AAAA,EAC7C,EAAE,MAAA,EAAQ,kBAAA,EAAoB,IAAA,EAAM,UAAA;AACtC,CAAA;AAE6B,IAAI,GAAA,CAAI,aAAA,CAAc,IAAI,CAAC,CAAA,KAAM,CAAA,CAAE,IAAI,CAAC;AA4C9D,SAAS,oBAAA,CACd,WACA,UAAA,EACoB;AACpB,EAAA,MAAM,KAAA,GAAQ,WAAW,IAAA,EAAK;AAC9B,EAAA,MAAM,MAAA,GAAS,YAAY,IAAA,EAAK;AAChC,EAAA,IAAI,SAAS,MAAA,EAAQ,OAAO,CAAA,EAAG,KAAK,IAAI,MAAM,CAAA,CAAA;AAC9C,EAAA,OAAO,SAAS,MAAA,IAAU,MAAA;AAC5B;;;ACkEO,SAAS,aAA6C,KAAA,EAA6B;AACxF,EAAA,OAAA,CAAQ,KAAA,IAAS,EAAC,EAAG,MAAA,CAAO,CAAC,CAAA,KAAM,CAAC,EAAE,MAAM,CAAA;AAC9C;;;ACvIO,IAAM,gBAAA,GAAmB,iBAAA;AASzB,SAAS,iBAAiB,OAAA,EAAqC;AACpE,EAAA,MAAM,QAAQ,OAAA,IAAW,gBAAA;AACzB,EAAA,IAAI,MAAM,KAAA,CAAM,MAAA;AAChB,EAAA,OAAO,GAAA,GAAM,KAAK,KAAA,CAAM,UAAA,CAAW,MAAM,CAAC,CAAA,KAAM,IAAM,GAAA,IAAO,CAAA;AAC7D,EAAA,OAAO,KAAA,CAAM,KAAA,CAAM,CAAA,EAAG,GAAG,CAAA;AAC3B;AAEO,SAAS,UAAA,CAAW,SAAiB,MAAA,EAAwB;AAClE,EAAA,OAAO,CAAA,EAAG,OAAO,CAAA,GAAA,EAAM,MAAM,CAAA,CAAA;AAC/B;;;AC2KA,IAAM,cAAA,GAAiB,QAAA;AAOvB,IAAM,cAAA,GAAiB,4BAAA;AAEhB,SAAS,aAAa,KAAA,EAA+C;AAC1E,EAAA,MAAM,OAAA,GAAU,OAAO,IAAA,EAAK;AAC5B,EAAA,IAAI,CAAC,SAAS,OAAO,MAAA;AACrB,EAAA,MAAM,KAAA,GAAQ,cAAA,CAAe,IAAA,CAAK,OAAO,CAAA;AACzC,EAAA,OAAO,KAAA,GAAQ,KAAA,CAAM,CAAC,CAAA,GAAI,MAAA;AAC5B;AAOA,IAAM,cAAA,GAAsD;AAAA,EAC1D,UAAA,EAAY,YAAA;AAAA,EACZ,eAAA,EAAiB,iBAAA;AAAA,EACjB,oBAAA,EAAsB,sBAAA;AAAA,EACtB,iBAAA,EAAmB,mBAAA;AAAA,EACnB,MAAA,EAAQ;AACV,CAAA;AAEA,IAAM,QAAA,GAAsB,CAAC,KAAA,KAAU,KAAA;AAGvC,SAAS,QAA0B,KAAA,EAAa;AAC9C,EAAA,OAAO,MAAA,CAAO,WAAA,CAAY,MAAA,CAAO,OAAA,CAAQ,KAAK,CAAA,CAAE,MAAA,CAAO,CAAC,GAAG,CAAC,CAAA,KAAM,CAAA,KAAM,MAAS,CAAC,CAAA;AACpF;AAGA,SAAS,OAAA,CAA6B,KAAQ,KAAA,EAAwC;AACpF,EAAA,OAAO,KAAA,CAAM,SAAS,CAAA,GAAK,EAAE,CAAC,GAAG,GAAG,OAAM,GAAuB,MAAA;AACnE;AAEA,SAAS,WAAA,CACP,UACA,CAAA,EACgC;AAChC,EAAA,IAAI,CAAC,UAAU,OAAO,MAAA;AACtB,EAAA,MAAM,SAAS,OAAA,CAAQ;AAAA,IACrB,MAAM,IAAA,CAAK,QAAA,CAAS,QAAA,IAAY,QAAA,CAAS,MAAM,CAAC,CAAA;AAAA,IAChD,MAAA,EAAQ,IAAA,CAAK,QAAA,CAAS,MAAA,EAAQ,CAAC,CAAA;AAAA,IAC/B,WAAA,EAAa,IAAA,CAAK,QAAA,CAAS,WAAA,EAAa,CAAC,CAAA;AAAA,IACzC,UAAA,EAAY,IAAA,CAAK,QAAA,CAAS,UAAA,EAAY,CAAC;AAAA,GACxC,CAAA;AACD,EAAA,OAAO,OAAO,IAAA,CAAK,MAAM,CAAA,CAAE,MAAA,GAAS,IAAI,MAAA,GAAS,MAAA;AACnD;AAGA,SAAS,IAAA,CAAK,OAA2B,CAAA,EAAkC;AACzE,EAAA,MAAM,OAAA,GAAU,OAAO,IAAA,EAAK;AAC5B,EAAA,OAAO,OAAA,GAAU,CAAA,CAAE,OAAO,CAAA,GAAI,MAAA;AAChC;AAOA,SAAS,OAAA,CACP,UAAA,EACA,MAAA,EACA,CAAA,EACoB;AACpB,EAAA,OAAO,KAAK,UAAA,EAAY,CAAC,CAAA,IAAK,IAAA,CAAK,QAAQ,CAAC,CAAA;AAC9C;AASO,SAAS,mBAAA,CACd,OAAA,EACA,OAAA,GAA6B,EAAC,EAClB;AACZ,EAAA,MAAM,CAAA,GAAI,QAAQ,QAAA,IAAY,QAAA;AAC9B,EAAA,MAAM,OAAA,GAAU,gBAAA,CAAiB,OAAA,CAAQ,OAAO,CAAA;AAChD,EAAA,MAAM,YAAY,OAAA,CAAQ,YAAA,IAAgB,UAAA,CAAW,OAAA,EAAS,QAAQ,MAAM,CAAA;AAE5E,EAAA,MAAM,IAAA,GACJ,oBAAA,CAAqB,OAAA,CAAQ,SAAA,EAAW,OAAA,CAAQ,UAAU,CAAA,IAC1D,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAa,CAAC,CAAA,IAC3B,OAAA,CAAQ,MAAA;AAEV,EAAA,MAAM,QAAA,GAAW,YAAA,CAAa,OAAA,CAAQ,gBAAgB,CAAA,CAAE,GAAA;AAAA,IAAI,CAAC,YAC3D,OAAA,CAA+B;AAAA,MAC7B,OAAA,EAAS,CAAA,CAAE,OAAA,CAAQ,QAAQ,CAAA;AAAA,MAC3B,QAAA,EAAU,IAAA,CAAK,OAAA,CAAQ,KAAA,EAAO,CAAC,CAAA;AAAA,MAC/B,GAAA,EAAK,IAAA,CAAK,OAAA,CAAQ,GAAA,EAAK,CAAC;AAAA,KACzB;AAAA,GACH;AAEA,EAAA,MAAM,SAAS,OAAA,CAA0B;AAAA,IACvC,IAAA;AAAA,IACA,KAAA,EAAO,IAAA,CAAK,OAAA,CAAQ,QAAA,EAAU,CAAC,CAAA;AAAA,IAC/B,KAAA,EAAO,IAAA,CAAK,OAAA,CAAQ,MAAA,EAAQ,CAAC,CAAA;AAAA,IAC7B,GAAA,EAAK,IAAA,CAAK,OAAA,CAAQ,OAAA,EAAS,CAAC,CAAA,IAAK,SAAA;AAAA,IACjC,OAAA,EAAS,IAAA,CAAK,OAAA,CAAQ,KAAA,EAAO,CAAC,CAAA;AAAA,IAC9B,QAAA,EAAU,WAAA,CAAY,OAAA,CAAQ,QAAA,EAAU,CAAC,CAAA;AAAA,IACzC,GAAG,OAAA,CAAQ,UAAA,EAAY,QAAQ;AAAA,GAChC,CAAA;AAED,EAAA,MAAM,IAAA,GAAO,YAAA,CAAa,OAAA,CAAQ,SAAS,CAAA,CAAE,GAAA;AAAA,IAAI,CAAC,aAChD,OAAA,CAAwB;AAAA,MACtB,MAAM,OAAA,CAAQ,QAAA,CAAS,UAAA,EAAY,QAAA,CAAS,SAAS,CAAC,CAAA;AAAA,MACtD,QAAA,EAAU,CAAA,CAAE,QAAA,CAAS,KAAK,CAAA;AAAA,MAC1B,OAAA,EAAS,IAAA,CAAK,QAAA,CAAS,WAAA,EAAa,CAAC,CAAA;AAAA,MACrC,SAAA,EAAW,YAAA,CAAa,QAAA,CAAS,SAAS,CAAA;AAAA,MAC1C,OAAA,EAAS,YAAA,CAAa,QAAA,CAAS,OAAO;AAAA,KACvC;AAAA,GACH;AAEA,EAAA,MAAM,SAAA,GAAY,YAAA,CAAa,OAAA,CAAQ,YAAY,CAAA,CAAE,GAAA;AAAA,IAAI,CAAC,UACxD,OAAA,CAA6B;AAAA,MAC3B,cAAc,OAAA,CAAQ,KAAA,CAAM,UAAA,EAAY,KAAA,CAAM,cAAc,CAAC,CAAA;AAAA,MAC7D,QAAA,EAAU,IAAA,CAAK,KAAA,CAAM,IAAA,EAAM,CAAC,CAAA;AAAA,MAC5B,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,WAAA,EAAa,CAAC,CAAA;AAAA,MAClC,SAAA,EAAW,YAAA,CAAa,KAAA,CAAM,SAAS,CAAA;AAAA,MACvC,OAAA,EAAS,YAAA,CAAa,KAAA,CAAM,OAAO;AAAA,KACpC;AAAA,GACH;AAEA,EAAA,MAAM,SAAA,GAAY,YAAA,CAAa,OAAA,CAAQ,SAAS,CAAA,CAAE,GAAA;AAAA,IAAI,CAAC,UACrD,OAAA,CAA6B;AAAA,MAC3B,aAAa,OAAA,CAAQ,KAAA,CAAM,UAAA,EAAY,KAAA,CAAM,aAAa,CAAC,CAAA;AAAA,MAC3D,IAAA,EAAM,IAAA,CAAK,KAAA,CAAM,YAAA,EAAc,CAAC,CAAA;AAAA,MAChC,SAAA,EAAW,IAAA,CAAK,KAAA,CAAM,MAAA,EAAQ,CAAC,CAAA;AAAA,MAC/B,SAAA,EAAW,YAAA,CAAa,KAAA,CAAM,SAAS,CAAA;AAAA,MACvC,OAAA,EAAS,YAAA,CAAa,KAAA,CAAM,OAAO;AAAA,KACpC;AAAA,GACH;AAEA,EAAA,MAAM,MAAA,GAAS,YAAA,CAAa,OAAA,CAAQ,MAAM,CAAA,CAAE,GAAA;AAAA,IAAI,CAAC,UAC/C,OAAA,CAAyB;AAAA,MACvB,KAAA,EAAO,CAAA,CAAE,KAAA,CAAM,KAAK,CAAA;AAAA,MACpB,IAAA,EAAM,YAAA,CAAa,KAAA,CAAM,IAAI,CAAA;AAAA,MAC7B,SAAS,OAAA,CAAQ,KAAA,CAAM,UAAA,EAAY,KAAA,CAAM,QAAQ,CAAC,CAAA;AAAA,MAClD,OAAA,EAAS,IAAA,CAAK,KAAA,CAAM,WAAA,EAAa,CAAC;AAAA,KACnC;AAAA,GACH;AAEA,EAAA,MAAM,YAAA,GAAe,YAAA,CAAa,OAAA,CAAQ,cAAc,CAAA,CAAE,GAAA;AAAA,IAAI,CAAC,SAC7D,OAAA,CAA+B;AAAA,MAC7B,IAAA,EAAM,CAAA,CAAE,IAAA,CAAK,IAAI,CAAA;AAAA,MACjB,IAAA,EAAM,YAAA,CAAa,IAAA,CAAK,SAAS,CAAA;AAAA;AAAA;AAAA,MAGjC,MAAA,EAAQ,QAAQ,IAAA,CAAK,UAAA,EAAY,KAAK,SAAA,IAAa,IAAA,CAAK,YAAY,CAAC,CAAA;AAAA,MACrE,GAAA,EAAK,IAAA,CAAK,IAAA,CAAK,aAAA,EAAe,CAAC;AAAA,KAChC;AAAA,GACH;AAEA,EAAA,MAAM,YAAA,GAAe,YAAA,CAAa,OAAA,CAAQ,YAAY,CAAA,CAAE,GAAA;AAAA,IAAI,CAAC,gBAC3D,OAAA,CAA+B;AAAA,MAC7B,IAAA,EAAM,CAAA,CAAE,WAAA,CAAY,KAAK,CAAA;AAAA,MACzB,SAAA,EAAW,IAAA,CAAK,WAAA,CAAY,SAAA,EAAW,CAAC,CAAA;AAAA,MACxC,WAAA,EAAa,YAAA,CAAa,WAAA,CAAY,IAAI,CAAA;AAAA,MAC1C,GAAA,EAAK,IAAA,CAAK,WAAA,CAAY,GAAA,EAAK,CAAC,CAAA;AAAA,MAC5B,OAAA,EAAS,IAAA,CAAK,WAAA,CAAY,WAAA,EAAa,CAAC;AAAA,KACzC;AAAA,GACH;AAOA,EAAA,MAAM,MAAA,GAAA,CAAU,OAAA,CAAQ,MAAA,IAAU,IAAI,GAAA,CAAI,CAAC,KAAA,MAAW,EAAE,IAAA,EAAM,CAAA,CAAE,KAAA,CAAM,IAAI,GAAE,CAAE,CAAA;AAE9E,EAAA,MAAM,SAAA,GAAY,YAAA,CAAa,OAAA,CAAQ,SAAS,CAAA,CAAE,GAAA;AAAA,IAAI,CAAC,aACrD,OAAA,CAA4B;AAAA,MAC1B,QAAA,EAAU,CAAA,CAAE,QAAA,CAAS,QAAQ,CAAA;AAAA,MAC7B,SAAS,QAAA,CAAS,WAAA,GAAc,cAAA,CAAe,QAAA,CAAS,WAAW,CAAA,GAAI;AAAA,KACxE;AAAA,GACH;AAEA,EAAA,MAAM,QAAA,GAAW,YAAA,CAAa,OAAA,CAAQ,QAAQ,CAAA,CAAE,GAAA;AAAA,IAAI,CAAC,YACnD,OAAA,CAA2B;AAAA,MACzB,IAAA,EAAM,CAAA,CAAE,OAAA,CAAQ,IAAI,CAAA;AAAA,MACpB,WAAA,EAAa,IAAA,CAAK,OAAA,CAAQ,WAAA,EAAa,CAAC,CAAA;AAAA,MACxC,GAAA,EAAK,IAAA,CAAK,OAAA,CAAQ,GAAA,EAAK,CAAC,CAAA;AAAA,MACxB,SAAA,EAAW,YAAA,CAAa,OAAA,CAAQ,SAAS,CAAA;AAAA,MACzC,OAAA,EAAS,YAAA,CAAa,OAAA,CAAQ,OAAO;AAAA,KACtC;AAAA,GACH;AAEA,EAAA,OAAO;AAAA,IACL,MAAA;AAAA,IACA,GAAG,OAAA,CAAQ,MAAA,EAAQ,IAAI,CAAA;AAAA,IACvB,GAAG,OAAA,CAAQ,WAAA,EAAa,SAAS,CAAA;AAAA,IACjC,GAAG,OAAA,CAAQ,WAAA,EAAa,SAAS,CAAA;AAAA,IACjC,GAAG,OAAA,CAAQ,QAAA,EAAU,MAAM,CAAA;AAAA,IAC3B,GAAG,OAAA,CAAQ,cAAA,EAAgB,YAAY,CAAA;AAAA,IACvC,GAAG,OAAA,CAAQ,cAAA,EAAgB,YAAY,CAAA;AAAA,IACvC,GAAG,OAAA,CAAQ,QAAA,EAAU,MAAM,CAAA;AAAA,IAC3B,GAAG,OAAA,CAAQ,WAAA,EAAa,SAAS,CAAA;AAAA,IACjC,GAAG,OAAA,CAAQ,UAAA,EAAY,QAAQ,CAAA;AAAA,IAC/B,IAAA,EAAM,EAAE,SAAA,EAAW,OAAA,EAAS,cAAA;AAAe,GAC7C;AACF","file":"index.js","sourcesContent":["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, skills, projects, talks,\n * publications), then the formal record (credentials, education, courses), then\n * the 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: 'skills', labelKey: 'skills', 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 {\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/** Whether a section has content for a given profile (used to hide empty nav entries for visitors). */\nexport function isSectionPopulated(profile: Profile, id: string): boolean {\n switch (id) {\n case 'about':\n return Boolean(profile.about && profile.headline);\n case 'career':\n return Boolean(profile.positions?.length);\n case 'education':\n return Boolean(profile.education?.length);\n case 'courses':\n return Boolean(profile.courses?.length);\n case 'skills':\n return Boolean(profile.skills?.length);\n case 'projects':\n return Boolean(profile.projects?.length);\n case 'credentials':\n return Boolean(profile.certifications?.length);\n case 'publications':\n return Boolean(profile.publications?.length);\n case 'presentations':\n return (\n Boolean(profile.presentations?.length) || Boolean(profile.presentationDeliveries?.length)\n );\n case 'involvement':\n return Boolean(profile.involvement?.length);\n case 'investments':\n return Boolean(profile.investments?.length);\n case 'awards':\n return Boolean(profile.honors?.length);\n case 'languages':\n return Boolean(profile.languages?.length);\n case 'other-profiles':\n return Boolean(profile.externalAccounts?.length);\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';\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\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 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 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/** 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\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 url: text(profile.website, s) ?? canonical,\n summary: text(profile.about, s),\n location: mapLocation(profile.location, 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"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
export { O as OrgClaimRequestInput,
|
|
2
|
+
export { O as OrgClaimRequestInput, a as OrgClaimRequestSchema, b as OrgDomainChallengeRequestInput, c as OrgDomainChallengeRequestSchema, d as OrgDomainVerifyRequestInput, e as OrgDomainVerifyRequestSchema, f as OrgNotificationEmailRequestInput, g as OrgNotificationEmailRequestSchema, h as OrgProfileUpdateRequestInput, i as OrgProfileUpdateRequestSchema } from '../../org-settings-B8e01iZx.cjs';
|
|
3
3
|
|
|
4
4
|
/** Prepend `https://` if no scheme is present. */
|
|
5
5
|
declare function normalizeUrl(val: string): string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
export { O as OrgClaimRequestInput,
|
|
2
|
+
export { O as OrgClaimRequestInput, a as OrgClaimRequestSchema, b as OrgDomainChallengeRequestInput, c as OrgDomainChallengeRequestSchema, d as OrgDomainVerifyRequestInput, e as OrgDomainVerifyRequestSchema, f as OrgNotificationEmailRequestInput, g as OrgNotificationEmailRequestSchema, h as OrgProfileUpdateRequestInput, i as OrgProfileUpdateRequestSchema } from '../../org-settings-B8e01iZx.js';
|
|
3
3
|
|
|
4
4
|
/** Prepend `https://` if no scheme is present. */
|
|
5
5
|
declare function normalizeUrl(val: string): string;
|
package/dist/{use-org-notification-emails-Cz4cuTyg.d.ts → use-org-notification-emails-CNDrIUcS.d.ts}
RENAMED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import * as react from 'react';
|
|
2
2
|
import { ReactNode } from 'react';
|
|
3
|
-
import { S as SifaApiConfig, s as sifaQueryKeys, W as WriteResult,
|
|
3
|
+
import { S as SifaApiConfig, s as sifaQueryKeys, W as WriteResult, av as RefreshPdsResult, aN as UpdateProfileOverrideInput, aO as UpdateProfileSelfInput, aP as UploadAvatarResult, aQ as UploadPronunciationAudioResult, C as CreateResult, aJ as SubCategoryBulkResult, aj as ProfileLocationInput, q as ExternalAccountInput, p as CreateExternalAccountResult, aR as VerifyExternalAccountResult, E as EndorsementInput, n as ConfirmationInput, o as ConfirmationSubjectInput, U as GivenConfirmation, ag as PendingConfirmationsPage, au as RefreshOrcidPublicationsResult, B as BulkHideProfileItemInput, a3 as HideProfileItemInput, _ as HiddenActivityItem, a2 as HideActivityItemInput, aM as UnhideActivityItemInput, aI as StatsResponse, k as AppRegistryEntry, $ as HiddenApp, d as AccountSummary, L as FilterOptions, aD as SearchFilters, aE as SearchResponse, aH as SkillSearchResult, cp as CompanySearchFilters, cq as CompanySearchResponse, r as FeaturedProfile, aG as SimilarProfile, K as FetchSuggestionsOptions, aL as SuggestionsResponse, P as FollowUserResult, w as FetchFollowListOptions, M as FollowListPage, Q as FollowingResponse, y as FetchFollowingFeedOptions, x as FetchFollowProfilePageOptions, O as FollowProfilePageResponse, F as FeatureAllowlistResponse, i as AdminReviewQueues, u as FetchActivityFeedOptions, e as ActivityFeedResponse, h as ActivityTeaserResponse, Z as HeatmapResponse, a5 as MyGithubPullRequestsResponse, c as AccountCheckResult, at as ReactionStatus, as as ReactionResult, ar as ReactionError, aC as RoadmapVotesResponse, l as CastRoadmapVoteResult, D as DeleteAccountResult, ax as ResetProfileResult, aS as WipePreview, aw as RepoDeleteInput, a7 as OrgClaimResult, ad as OrgProfileUpdateResult, a8 as OrgDomainChallengeResult, a9 as OrgDomainVerifyResult, aa as OrgNotificationEmailAddResult, ab as OrgNotificationEmailRemoveResult } from './org-DS183N90.js';
|
|
4
4
|
import * as _tanstack_react_query from '@tanstack/react-query';
|
|
5
5
|
import { UseQueryOptions, UseMutationOptions, UseInfiniteQueryOptions, InfiniteData } from '@tanstack/react-query';
|
|
6
6
|
import { c as Profile, d as ProfilePosition, S as SkillRef, p as ExternalAccount, Q as SkillSuggestion, s as OrgFloorVerdict } from './index-CTzpbW81.js';
|
|
7
7
|
import { n as ProfileView, t as RepoDeleteResult, w as RepoInventory } from './types-zjv3qDWz.js';
|
|
8
8
|
import { j as EntitySearchResponse, E as EntitySearchResult, d as EntityMintDomainResponse, h as EntityResolveDomainResponse, o as EntitySelectResponse, m as EntitySelectRequest, y as FollowFeedPage, s as FeatureFlag } from './feed-BMm9Pkew.js';
|
|
9
|
-
import { O as OrgClaimRequestInput,
|
|
9
|
+
import { O as OrgClaimRequestInput, h as OrgProfileUpdateRequestInput, b as OrgDomainChallengeRequestInput, d as OrgDomainVerifyRequestInput, f as OrgNotificationEmailRequestInput } from './org-settings-B8e01iZx.js';
|
|
10
10
|
|
|
11
11
|
interface SifaProviderProps {
|
|
12
12
|
config: SifaApiConfig;
|
|
@@ -298,6 +298,19 @@ declare const useBulkHideProfileItems: (ownerHandleOrDid: string, options?: Omit
|
|
|
298
298
|
/** Bulk-unhide profile items sharing the same `itemType` + `source`. */
|
|
299
299
|
declare const useBulkUnhideProfileItems: (ownerHandleOrDid: string, options?: Omit<UseMutationOptions<WriteResult, Error, BulkHideProfileItemInput, unknown>, "mutationFn"> | undefined) => _tanstack_react_query.UseMutationResult<WriteResult, Error, BulkHideProfileItemInput, unknown>;
|
|
300
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Everything the authenticated user has hidden from their activity, for the
|
|
303
|
+
* unhide list in settings.
|
|
304
|
+
*/
|
|
305
|
+
declare function useHiddenActivityItems(options?: Omit<UseQueryOptions<HiddenActivityItem[], Error, HiddenActivityItem[], ReturnType<typeof sifaQueryKeys.activity.hiddenItems>>, 'queryKey' | 'queryFn'>): _tanstack_react_query.UseQueryResult<NoInfer<HiddenActivityItem[]>, Error>;
|
|
306
|
+
/**
|
|
307
|
+
* Hide one activity item from the caller's own profile. The record stays on
|
|
308
|
+
* their PDS; only its appearance on sifa.id is suppressed.
|
|
309
|
+
*/
|
|
310
|
+
declare const useHideActivityItem: (options?: Omit<UseMutationOptions<WriteResult, Error, HideActivityItemInput, unknown>, "mutationFn"> | undefined) => _tanstack_react_query.UseMutationResult<WriteResult, Error, HideActivityItemInput, unknown>;
|
|
311
|
+
/** Restore a previously-hidden activity item. */
|
|
312
|
+
declare const useUnhideActivityItem: (options?: Omit<UseMutationOptions<WriteResult, Error, UnhideActivityItemInput, unknown>, "mutationFn"> | undefined) => _tanstack_react_query.UseMutationResult<WriteResult, Error, UnhideActivityItemInput, unknown>;
|
|
313
|
+
|
|
301
314
|
/**
|
|
302
315
|
* React hook for the public homepage stats. Returns `null` data on error.
|
|
303
316
|
*/
|
|
@@ -838,4 +851,4 @@ declare function useRemoveOrgNotificationEmail(options?: Omit<UseMutationOptions
|
|
|
838
851
|
email: string;
|
|
839
852
|
}, unknown>;
|
|
840
853
|
|
|
841
|
-
export { useDeleteReaction as $, type AddFeatureAllowlistVariables as A, useCanonicalSkillSearch as B, type CreateReactionVariables as C, type DeleteReactionVariables as D, useCastRoadmapVote as E, type FollowVariables as F, useCreateConfirmation as G, useCreateEducation as H, useCreateEndorsement as I, useCreateExternalAccount as J, useCreatePosition as K, useCreateProfileLocation as L, useCreateReaction as M, useCreateRecord as N, useCreateSkill as O, type PositionSkillLinkVariables as P, useDebouncedValue as Q, type RemoveFeatureAllowlistVariables as R, SifaProvider as S, useDeleteAccount as T, type UnfollowVariables as U, useDeleteAvatarOverride as V, useDeleteEducation as W, useDeleteExternalAccount as X, useDeleteNamePronunciationAudio as Y, useDeletePosition as Z, useDeleteProfileLocation as _, type CreateRecordVariables as a,
|
|
854
|
+
export { useDeleteReaction as $, type AddFeatureAllowlistVariables as A, useCanonicalSkillSearch as B, type CreateReactionVariables as C, type DeleteReactionVariables as D, useCastRoadmapVote as E, type FollowVariables as F, useCreateConfirmation as G, useCreateEducation as H, useCreateEndorsement as I, useCreateExternalAccount as J, useCreatePosition as K, useCreateProfileLocation as L, useCreateReaction as M, useCreateRecord as N, useCreateSkill as O, type PositionSkillLinkVariables as P, useDebouncedValue as Q, type RemoveFeatureAllowlistVariables as R, SifaProvider as S, useDeleteAccount as T, type UnfollowVariables as U, useDeleteAvatarOverride as V, useDeleteEducation as W, useDeleteExternalAccount as X, useDeleteNamePronunciationAudio as Y, useDeletePosition as Z, useDeleteProfileLocation as _, type CreateRecordVariables as a, useUnhideKeytraceClaim as a$, useDeleteRecord as a0, useDeleteRepoRecords as a1, useDeleteSkill as a2, useDismissConfirmation as a3, useEndorsementCount as a4, useEntitySearch as a5, useExternalAccounts as a6, useFeatureAllowlist as a7, useFeaturedProfile as a8, useFollow as a9, usePendingConfirmations as aA, useProfile as aB, useReactionStatus as aC, useRefreshOrcidPublications as aD, useRefreshPds as aE, useRemoveFeatureAllowlist as aF, useRemoveOrgNotificationEmail as aG, useRepoInventory as aH, useResetProfile as aI, useResolveEntityDomain as aJ, useRetractRoadmapVote as aK, useRevealMarqueDomain as aL, useRevokeConfirmation as aM, useRoadmapVotes as aN, useSearchFilters as aO, useSearchProfiles as aP, useSelectEntity as aQ, useSetExternalAccountPrimary as aR, useSetPositionPrimary as aS, useSifaConfig as aT, useSimilarProfiles as aU, useSkillSuggestions as aV, useStats as aW, useSuggestionCount as aX, useSuggestions as aY, useUnfollow as aZ, useUnhideActivityItem as a_, useFollowers as aa, useFollowing as ab, useFollowingFeed as ac, useFollowingList as ad, useGetProfileView as ae, useGivenConfirmations as af, useHeatmapData as ag, useHiddenActivityItems as ah, useHiddenApps as ai, useHideActivityItem as aj, useHideKeytraceClaim as ak, useHideOrcidPublication as al, useHideProfileItem as am, useHideSifaPublication as an, useHideStandardPublication as ao, useImportSearchEntities as ap, useLinkSkillToPosition as aq, useMintEntityDomain as ar, useMutuals as as, useMyGithubPullRequests as at, useMyRoadmapVotes as au, useNetworkStreamCount as av, useOrgClaim as aw, useOrgDomainChallenge as ax, useOrgDomainVerify as ay, useOrgProfile as az, type DeleteRecordVariables as b, useUnhideOrcidPublication as b0, useUnhideProfileItem as b1, useUnhideSifaPublication as b2, useUnhideStandardPublication as b3, useUnlinkSkillFromPosition as b4, useUnrevealMarqueDomain as b5, useUnsetExternalAccountPrimary as b6, useUnsetPositionPrimary as b7, useUpdateEducation as b8, useUpdateExternalAccount as b9, useUpdateOrgProfile as ba, useUpdatePosition as bb, useUpdateProfileLocation as bc, useUpdateProfileOverride as bd, useUpdateProfileSelf as be, useUpdateRecord as bf, useUpdateSkill as bg, useUpdateSkillSubCategories as bh, useUploadAvatar as bi, useUploadNamePronunciationAudio as bj, useVerifyExternalAccount as bk, useWipePreview as bl, useSearchCompanies as bm, type SifaProviderProps as c, type UpdateEducationVariables as d, type UpdateExternalAccountVariables as e, type UpdatePositionVariables as f, type UpdateProfileLocationVariables as g, type UpdateRecordVariables as h, type UpdateSkillSubCategoriesVariables as i, type UpdateSkillVariables as j, type UseEntitySearchOptions as k, type UseMyGithubPullRequestsParams as l, useActivityFeed as m, useActivityTeaser as n, useAddFeatureAllowlist as o, useAddOrgNotificationEmail as p, useAdminReviewQueues as q, useAppAccountCheck as r, useAppsRegistry as s, useAtFundLink as t, useAccounts as u, useBlueskySuggestions as v, useBulkHideProfileItems as w, useBulkHideStandardPublications as x, useBulkUnhideProfileItems as y, useBulkUnhideStandardPublications as z };
|