myst-frontmatter
Advanced tools
Comparing version 1.1.1 to 1.1.2
import type { CreditRole } from 'credit-roles'; | ||
import type { Licenses } from '../licenses/types.js'; | ||
export interface Affiliation { | ||
id?: string; | ||
name?: string; | ||
institution?: string; | ||
department?: string; | ||
address?: string; | ||
city?: string; | ||
state?: string; | ||
postal_code?: string; | ||
country?: string; | ||
collaboration?: boolean; | ||
isni?: string; | ||
ringgold?: number; | ||
ror?: string; | ||
url?: string; | ||
email?: string; | ||
phone?: string; | ||
fax?: string; | ||
} | ||
export type AuthorRoles = CreditRole | string; | ||
export interface Author { | ||
id?: string; | ||
name?: string; | ||
@@ -9,10 +29,23 @@ userId?: string; | ||
corresponding?: boolean; | ||
equal_contributor?: boolean; | ||
deceased?: boolean; | ||
email?: string; | ||
roles?: AuthorRoles[]; | ||
affiliations?: string[]; | ||
collaborations?: string[]; | ||
twitter?: string; | ||
github?: string; | ||
website?: string; | ||
url?: string; | ||
note?: string; | ||
phone?: string; | ||
fax?: string; | ||
} | ||
/** | ||
* Object to hold items referenced in multiple parts of frontmatter | ||
* | ||
* These will be normalized to the top level and replaced with ids elsewhere | ||
*/ | ||
export type ReferenceStash = { | ||
affiliations?: Affiliation[]; | ||
authors?: Author[]; | ||
}; | ||
export type Biblio = { | ||
@@ -115,2 +148,3 @@ volume?: string | number; | ||
authors?: Author[]; | ||
affiliations?: Affiliation[]; | ||
venue?: Venue; | ||
@@ -117,0 +151,0 @@ github?: string; |
import type { ValidationOptions } from 'simple-validators'; | ||
import type { Author, Biblio, Export, Jupytext, KernelSpec, Numbering, PageFrontmatter, ProjectFrontmatter, SiteFrontmatter, Venue, Thebe, BinderHubOptions, JupyterServerOptions, JupyterLocalOptions } from './types.js'; | ||
import type { Author, Biblio, Export, Jupytext, KernelSpec, Numbering, PageFrontmatter, ProjectFrontmatter, SiteFrontmatter, Venue, Thebe, BinderHubOptions, JupyterServerOptions, JupyterLocalOptions, ReferenceStash, Affiliation } from './types.js'; | ||
export declare const SITE_FRONTMATTER_KEYS: string[]; | ||
@@ -9,2 +9,20 @@ export declare const PROJECT_FRONTMATTER_KEYS: string[]; | ||
/** | ||
* Update stash of authors/affiliations based on input value | ||
* | ||
* Input may be: | ||
* - string name | ||
* - string id | ||
* - object without id | ||
* - object with id | ||
* | ||
* This function will normalize all of the above to an id and if a corresponding | ||
* object does not yet exist in the stash, it will be added. The id is returned. | ||
* | ||
* This function will warn if two objects are explicitly defined with the same id. | ||
*/ | ||
export declare function validateAndStashObject<T extends { | ||
id?: string; | ||
name?: string; | ||
}>(input: any, stash: ReferenceStash, kind: keyof ReferenceStash, validateFn: (v: any, o: ValidationOptions) => T | undefined, opts: ValidationOptions): string | undefined; | ||
/** | ||
* Validate Venue object against the schema | ||
@@ -16,5 +34,12 @@ * | ||
/** | ||
* Validate Affiliation object against the schema | ||
*/ | ||
export declare function validateAffiliation(input: any, opts: ValidationOptions): Affiliation | { | ||
id: string; | ||
name: string; | ||
} | undefined; | ||
/** | ||
* Validate Author object against the schema | ||
*/ | ||
export declare function validateAuthor(input: any, opts: ValidationOptions): Author | undefined; | ||
export declare function validateAuthor(input: any, stash: ReferenceStash, opts: ValidationOptions): Author | undefined; | ||
/** | ||
@@ -70,3 +95,3 @@ * Validate Biblio object | ||
*/ | ||
export declare function fillPageFrontmatter(pageFrontmatter: PageFrontmatter, projectFrontmatter: ProjectFrontmatter): PageFrontmatter; | ||
export declare function fillPageFrontmatter(pageFrontmatter: PageFrontmatter, projectFrontmatter: ProjectFrontmatter, opts: ValidationOptions): PageFrontmatter; | ||
/** | ||
@@ -73,0 +98,0 @@ * Unnest `kernelspec` from `jupyter.kernelspec` |
import { doi } from 'doi-utils'; | ||
import { credit } from 'credit-roles'; | ||
import { orcid } from 'orcid'; | ||
import { defined, incrementOptions, fillMissingKeys, filterKeys, validateBoolean, validateDate, validateEmail, validateEnum, validateKeys, validateList, validateObject, validateObjectKeys, validateString, validateUrl, validationError, validationWarning, } from 'simple-validators'; | ||
import { defined, incrementOptions, fillMissingKeys, filterKeys, validateBoolean, validateDate, validateEmail, validateEnum, validateKeys, validateList, validateObject, validateObjectKeys, validateString, validateUrl, validationError, validationWarning, validateNumber, } from 'simple-validators'; | ||
import { validateLicenses } from '../licenses/validators.js'; | ||
import { BinderProviders, ExportFormats } from './types.js'; | ||
import { createHash } from 'node:crypto'; | ||
export const SITE_FRONTMATTER_KEYS = [ | ||
@@ -20,2 +21,3 @@ 'title', | ||
'keywords', | ||
'affiliations', | ||
]; | ||
@@ -60,3 +62,23 @@ export const PROJECT_FRONTMATTER_KEYS = [ | ||
]; | ||
const AFFILIATION_KEYS = [ | ||
'id', | ||
'address', | ||
'city', | ||
'state', | ||
'postal_code', | ||
'country', | ||
'name', | ||
'institution', | ||
'department', | ||
'collaboration', | ||
'isni', | ||
'ringgold', | ||
'ror', | ||
'url', | ||
'email', | ||
'phone', | ||
'fax', | ||
]; | ||
const AUTHOR_KEYS = [ | ||
'id', | ||
'userId', | ||
@@ -66,2 +88,4 @@ 'name', | ||
'corresponding', | ||
'equal_contributor', | ||
'deceased', | ||
'email', | ||
@@ -73,3 +97,6 @@ 'roles', | ||
'github', | ||
'website', | ||
'url', | ||
'note', | ||
'phone', | ||
'fax', | ||
]; | ||
@@ -79,3 +106,10 @@ const AUTHOR_ALIASES = { | ||
affiliation: 'affiliations', | ||
website: 'url', | ||
}; | ||
const AFFILIATION_ALIASES = { | ||
ref: 'id', | ||
region: 'state', | ||
province: 'state', | ||
website: 'url', | ||
}; | ||
const BIBLIO_KEYS = ['volume', 'issue', 'first_page', 'last_page']; | ||
@@ -145,3 +179,68 @@ const THEBE_KEYS = [ | ||
} | ||
function stashPlaceholder(value) { | ||
return { id: value, name: value }; | ||
} | ||
/** | ||
* Return true if object: | ||
* - has 2 keys and only 2 keys: id and name | ||
* - the values for id and name are the same | ||
*/ | ||
function isStashPlaceholder(object) { | ||
return Object.keys(object).length === 2 && object.name && object.id && object.name === object.id; | ||
} | ||
function normalizedString(value) { | ||
return JSON.stringify(Object.entries(value).sort()); | ||
} | ||
/** | ||
* Update stash of authors/affiliations based on input value | ||
* | ||
* Input may be: | ||
* - string name | ||
* - string id | ||
* - object without id | ||
* - object with id | ||
* | ||
* This function will normalize all of the above to an id and if a corresponding | ||
* object does not yet exist in the stash, it will be added. The id is returned. | ||
* | ||
* This function will warn if two objects are explicitly defined with the same id. | ||
*/ | ||
export function validateAndStashObject(input, stash, kind, validateFn, opts) { | ||
var _a; | ||
const lookup = {}; | ||
(_a = stash[kind]) === null || _a === void 0 ? void 0 : _a.forEach((item) => { | ||
if (item.id) | ||
lookup[item.id] = item; | ||
}); | ||
if (typeof input === 'string' && Object.keys(lookup).includes(input)) { | ||
// Handle case where input is id and object already exists | ||
return input; | ||
} | ||
const value = validateFn(input, opts); | ||
if (!value) | ||
return; | ||
// Only warn on duplicate if the new object is not a placeholder | ||
let warnOnDuplicate = !isStashPlaceholder(value); | ||
if (!value.id) { | ||
// If object is defined without an id, generate a unique id | ||
value.id = createHash('md5').update(normalizedString(value)).digest('hex'); | ||
// Do not warn on duplicates for hash ids; any duplicates here are identical | ||
warnOnDuplicate = false; | ||
} | ||
if (!Object.keys(lookup).includes(value.id)) { | ||
// Handle case of new id - add stash value | ||
lookup[value.id] = value; | ||
} | ||
else if (isStashPlaceholder(lookup[value.id])) { | ||
// Handle case of existing placeholder { id: value, name: value } - replace stash value | ||
lookup[value.id] = value; | ||
} | ||
else if (warnOnDuplicate) { | ||
// Warn on duplicate id - lose new object | ||
validationWarning(`duplicate id for ${kind} found in frontmatter: ${value.id}`, opts); | ||
} | ||
stash[kind] = Object.values(lookup); | ||
return value.id; | ||
} | ||
/** | ||
* Validate Venue object against the schema | ||
@@ -174,7 +273,84 @@ * | ||
/** | ||
* Validate Affiliation object against the schema | ||
*/ | ||
export function validateAffiliation(input, opts) { | ||
if (typeof input === 'string') { | ||
input = stashPlaceholder(input); | ||
} | ||
const value = validateObjectKeys(input, { optional: AFFILIATION_KEYS, alias: AFFILIATION_ALIASES }, opts); | ||
if (value === undefined) | ||
return undefined; | ||
const output = {}; | ||
if (defined(value.id)) { | ||
output.id = validateString(value.id, incrementOptions('id', opts)); | ||
} | ||
if (defined(value.name)) { | ||
output.name = validateString(value.name, incrementOptions('name', opts)); | ||
} | ||
if (defined(value.institution)) { | ||
output.institution = validateString(value.institution, incrementOptions('institution', opts)); | ||
} | ||
if (defined(value.department)) { | ||
output.department = validateString(value.department, incrementOptions('department', opts)); | ||
} | ||
if (defined(value.address)) { | ||
output.address = validateString(value.address, incrementOptions('address', opts)); | ||
} | ||
if (defined(value.city)) { | ||
output.city = validateString(value.city, incrementOptions('city', opts)); | ||
} | ||
if (defined(value.state)) { | ||
output.state = validateString(value.state, incrementOptions('state', opts)); | ||
} | ||
if (defined(value.postal_code)) { | ||
output.postal_code = validateString(value.postal_code, incrementOptions('postal_code', opts)); | ||
} | ||
if (defined(value.country)) { | ||
output.country = validateString(value.country, incrementOptions('country', opts)); | ||
} | ||
// Both ISNI and ROR validation should occur similar to orcid (maybe in that same lib?) | ||
if (defined(value.isni)) { | ||
output.isni = validateString(value.isni, incrementOptions('isni', opts)); | ||
} | ||
if (defined(value.ror)) { | ||
output.ror = validateString(value.ror, incrementOptions('ror', opts)); | ||
} | ||
if (defined(value.ringgold)) { | ||
output.ringgold = validateNumber(value.ringgold, { | ||
min: 1000, | ||
max: 999999, | ||
...incrementOptions('ringgold', opts), | ||
}); | ||
} | ||
if (defined(value.collaboration)) { | ||
output.collaboration = validateBoolean(value.collaboration, incrementOptions('collaboration', opts)); | ||
} | ||
if (defined(value.email)) { | ||
output.email = validateEmail(value.email, incrementOptions('email', opts)); | ||
} | ||
if (defined(value.url)) { | ||
output.url = validateUrl(value.url, incrementOptions('url', opts)); | ||
} | ||
if (defined(value.phone)) { | ||
output.phone = validateString(value.phone, incrementOptions('phone', opts)); | ||
} | ||
if (defined(value.fax)) { | ||
output.fax = validateString(value.fax, incrementOptions('fax', opts)); | ||
} | ||
// If affiliation only has an id, give it a matching name; this is equivalent to the case | ||
// where a simple string is provided as an affiliation. | ||
if (Object.keys(output).length === 1 && output.id) { | ||
return stashPlaceholder(output.id); | ||
} | ||
else if (!output.name && !output.institution) { | ||
validationWarning('affiliation should include name or institution', opts); | ||
} | ||
return output; | ||
} | ||
/** | ||
* Validate Author object against the schema | ||
*/ | ||
export function validateAuthor(input, opts) { | ||
export function validateAuthor(input, stash, opts) { | ||
if (typeof input === 'string') { | ||
input = { name: input }; | ||
input = { id: input, name: input }; | ||
} | ||
@@ -185,2 +361,5 @@ const value = validateObjectKeys(input, { optional: AUTHOR_KEYS, alias: AUTHOR_ALIASES }, opts); | ||
const output = {}; | ||
if (defined(value.id)) { | ||
output.id = validateString(value.id, incrementOptions('id', opts)); | ||
} | ||
if (defined(value.userId)) { | ||
@@ -193,2 +372,5 @@ // TODO: Better userId validation - length? regex? | ||
} | ||
else { | ||
validationWarning('author should include name', opts); | ||
} | ||
if (defined(value.orcid)) { | ||
@@ -212,2 +394,8 @@ const orcidOpts = incrementOptions('orcid', opts); | ||
} | ||
if (defined(value.equal_contributor)) { | ||
output.equal_contributor = validateBoolean(value.equal_contributor, incrementOptions('equal_contributor', opts)); | ||
} | ||
if (defined(value.deceased)) { | ||
output.deceased = validateBoolean(value.deceased, incrementOptions('deceased', opts)); | ||
} | ||
if (defined(value.email)) { | ||
@@ -234,2 +422,5 @@ output.email = validateEmail(value.email, incrementOptions('email', opts)); | ||
} | ||
if (defined(value.collaborations)) { | ||
validationError('collaborations must be defined in frontmatter as affiliations with "collaboration: true"', incrementOptions('collaborations', opts)); | ||
} | ||
if (defined(value.affiliations)) { | ||
@@ -239,20 +430,8 @@ const affiliationsOpts = incrementOptions('affiliations', opts); | ||
if (typeof affiliations === 'string') { | ||
affiliations = affiliations.split(';'); | ||
affiliations = affiliations.split(';').map((aff) => aff.trim()); | ||
} | ||
output.affiliations = validateList(affiliations, affiliationsOpts, (aff) => { | ||
var _a; | ||
return (_a = validateString(aff, affiliationsOpts)) === null || _a === void 0 ? void 0 : _a.trim(); | ||
return validateAndStashObject(aff, stash, 'affiliations', validateAffiliation, affiliationsOpts); | ||
}); | ||
} | ||
if (defined(value.collaborations)) { | ||
const collaborationsOpts = incrementOptions('collaborations', opts); | ||
let collaborations = value.collaborations; | ||
if (typeof collaborations === 'string') { | ||
collaborations = collaborations.split(';'); | ||
} | ||
output.collaborations = validateList(collaborations, collaborationsOpts, (col) => { | ||
var _a; | ||
return (_a = validateString(col, collaborationsOpts)) === null || _a === void 0 ? void 0 : _a.trim(); | ||
}); | ||
} | ||
if (defined(value.twitter)) { | ||
@@ -264,5 +443,14 @@ output.twitter = validateString(value.twitter, incrementOptions('twitter', opts)); | ||
} | ||
if (defined(value.website)) { | ||
output.website = validateUrl(value.website, incrementOptions('website', opts)); | ||
if (defined(value.url)) { | ||
output.url = validateUrl(value.url, incrementOptions('url', opts)); | ||
} | ||
if (defined(value.phone)) { | ||
output.phone = validateString(value.phone, incrementOptions('phone', opts)); | ||
} | ||
if (defined(value.fax)) { | ||
output.fax = validateString(value.fax, incrementOptions('fax', opts)); | ||
} | ||
if (defined(value.note)) { | ||
output.note = validateString(value.note, incrementOptions('note', opts)); | ||
} | ||
return output; | ||
@@ -623,2 +811,13 @@ } | ||
} | ||
const stash = {}; | ||
if (defined(value.affiliations)) { | ||
const affiliationsOpts = incrementOptions('affiliations', opts); | ||
let affiliations = value.affiliations; | ||
if (typeof affiliations === 'string') { | ||
affiliations = affiliations.split(';').map((aff) => aff.trim()); | ||
} | ||
validateList(affiliations, affiliationsOpts, (aff) => { | ||
return validateAndStashObject(aff, stash, 'affiliations', validateAffiliation, affiliationsOpts); | ||
}); | ||
} | ||
if (defined(value.authors)) { | ||
@@ -631,3 +830,3 @@ let authors = value.authors; | ||
output.authors = validateList(authors, incrementOptions('authors', opts), (author, index) => { | ||
return validateAuthor(author, incrementOptions(`authors.${index}`, opts)); | ||
return validateAuthor(author, stash, incrementOptions(`authors.${index}`, opts)); | ||
}); | ||
@@ -656,2 +855,5 @@ // Ensure there is a corresponding author if an email is provided | ||
} | ||
if (stash.affiliations) { | ||
output.affiliations = stash.affiliations; | ||
} | ||
return output; | ||
@@ -838,3 +1040,3 @@ } | ||
*/ | ||
export function fillPageFrontmatter(pageFrontmatter, projectFrontmatter) { | ||
export function fillPageFrontmatter(pageFrontmatter, projectFrontmatter, opts) { | ||
var _a, _b, _c, _d; | ||
@@ -859,2 +1061,37 @@ const frontmatter = fillMissingKeys(pageFrontmatter, projectFrontmatter, USE_PROJECT_FALLBACK); | ||
} | ||
// Replace affiliation placeholders with extra affiliations available on the project/page | ||
let affiliations; | ||
let extraAffiliations; | ||
// Currently, affiliations are connected only to authors, so we look at | ||
// which frontmatter (project or page) has authors defined, and use the | ||
// affiliations from there. However, we still use the other affiliations | ||
// to fill out any placeholders where affiliations have id only. | ||
if (projectFrontmatter.authors && !pageFrontmatter.authors) { | ||
affiliations = projectFrontmatter.affiliations; | ||
extraAffiliations = pageFrontmatter.affiliations; | ||
} | ||
else { | ||
affiliations = pageFrontmatter.affiliations; | ||
extraAffiliations = projectFrontmatter.affiliations; | ||
} | ||
if (affiliations) { | ||
const projectAffLookup = {}; | ||
extraAffiliations === null || extraAffiliations === void 0 ? void 0 : extraAffiliations.forEach((aff) => { | ||
if (aff.id && !isStashPlaceholder(aff)) { | ||
projectAffLookup[aff.id] = aff; | ||
} | ||
}); | ||
frontmatter.affiliations = affiliations.map((aff) => { | ||
if (!aff.id || !projectAffLookup[aff.id]) { | ||
return aff; | ||
} | ||
else if (isStashPlaceholder(aff)) { | ||
return projectAffLookup[aff.id]; | ||
} | ||
else if (normalizedString(aff) !== normalizedString(projectAffLookup[aff.id])) { | ||
validationWarning(`Duplicate affiliation id within project: ${aff.id}`, incrementOptions('affiliations', opts)); | ||
} | ||
return aff; | ||
}); | ||
} | ||
return frontmatter; | ||
@@ -861,0 +1098,0 @@ } |
{ | ||
"name": "myst-frontmatter", | ||
"version": "1.1.1", | ||
"version": "1.1.2", | ||
"sideEffects": false, | ||
@@ -35,3 +35,3 @@ "license": "MIT", | ||
"dependencies": { | ||
"credit-roles": "^2.0.0", | ||
"credit-roles": "^2.1.0", | ||
"doi-utils": "^2.0.0", | ||
@@ -38,0 +38,0 @@ "orcid": "^1.0.0", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
New author
Supply chain riskA new npm collaborator published a version of the package for the first time. New collaborators are usually benign additions to a project, but do indicate a change to the security surface area of a package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
115531
3456
1
Updatedcredit-roles@^2.1.0