Comparing version 6.25.0 to 7.0.0-next.1703075602.d3e1a4be5066f75a2b484559e0c4f5b5a631a61b
@@ -0,1 +1,58 @@ | ||
/** GitHub Sponsors URL */ | ||
function getUsernameFromGitHubSponsorsUrl(url) { | ||
const match = /^https?:\/\/github\.com\/sponsors\/([^/]+)\/?$/.exec(url); | ||
return (match && match[1]) || ''; | ||
} | ||
/** GitHub URL */ | ||
function getUsernameFromGitHubUrl(url) { | ||
const match = /^https?:\/\/github\.com\/([^/]+)\/?$/.exec(url); | ||
return (match && match[1]) || ''; | ||
} | ||
/** Gist URL */ | ||
function getUsernameFromGistUrl(url) { | ||
const match = /^https?:\/\/gist\.github\.com\/([^/]+)\/?$/.exec(url); | ||
return (match && match[1]) || ''; | ||
} | ||
/** GitLab URL */ | ||
function getUsernameFromGitLabUrl(url) { | ||
const match = /^https?:\/\/gitlab\.com\/([^/]+)\/?$/.exec(url); | ||
return (match && match[1]) || ''; | ||
} | ||
/** ThanksDev GitHub URL */ | ||
function getGitHubUsernameFromThanksDevUrl(url) { | ||
const match = /^https?:\/\/thanks\.dev\/d\/gh\/([^/]+)\/?$/.exec(url); | ||
return (match && match[1]) || ''; | ||
} | ||
/** ThanksDev GitLab URL */ | ||
function getGitLabUsernameFromThanksDevUrl(url) { | ||
const match = /^https?:\/\/thanks\.dev\/d\/gl\/([^/]+)\/?$/.exec(url); | ||
return (match && match[1]) || ''; | ||
} | ||
/** Facebook URL */ | ||
function getUsernameFromFacebookUrl(url) { | ||
const match = /^https?:\/\/facebook\.com\/([^/]+)\/?$/.exec(url); | ||
return (match && match[1]) || ''; | ||
} | ||
/** Twitter URL */ | ||
function getUsernameFromTwitterUrl(url) { | ||
const match = /^https?:\/\/twitter\.com\/([^/]+)\/?$/.exec(url); | ||
return (match && match[1]) || ''; | ||
} | ||
/** Patreon URL */ | ||
function getUsernameFromPatreonUrl(url) { | ||
const match = /^https?:\/\/patreon\.com\/([^/]+)\/?$/.exec(url); | ||
return (match && match[1]) || ''; | ||
} | ||
/** OpenCollective URL */ | ||
function getUsernameFromOpenCollectiveUrl(url) { | ||
const match = /^https?:\/\/opencollective\.com\/([^/]+)\/?$/.exec(url); | ||
return (match && match[1]) || ''; | ||
} | ||
/** Trim a value if it is a string */ | ||
function trim(value) { | ||
if (typeof value === 'string') { | ||
return value.trim(); | ||
} | ||
return value; | ||
} | ||
/** Comparator for sorting fellows in an array */ | ||
@@ -7,165 +64,357 @@ export function comparator(a, b) { | ||
export default class Fellow { | ||
/** Actual name that is stored, otherwise falls back to username from the url fields */ | ||
_name; | ||
/** Years active for the current repository, extracted from the name */ | ||
years; | ||
/** URLs used */ | ||
urls = new Set(); | ||
/** Emails used */ | ||
emails = new Set(); | ||
/** Map of repository slugs with the contributions from the user */ | ||
contributions = new Map(); | ||
/** Set of repository slugs that the fellow administers to */ | ||
administeredRepositories = new Set(); | ||
/** Set of repository slugs that the fellow contributes to */ | ||
contributedRepositories = new Set(); | ||
/** Set of repository slugs that the fellow maintains */ | ||
maintainedRepositories = new Set(); | ||
/** Set of repository slugs that the fellow authors */ | ||
authoredRepositories = new Set(); | ||
/** | ||
* An array of field names that are used to determine if two fellow's are the same. | ||
* Don't need to add usernames for github, twitter, and facebook, as they will be compared via `urls`. | ||
* Can't compare just username, as that is not unique unless comparison's are on the same service, hence why `urls` are used and not `usernames`. | ||
*/ | ||
idFields = ['urls', 'emails']; | ||
/** An array of field names that are used to determine the fellow's URL */ | ||
urlFields = [ | ||
'url', | ||
'homepage', | ||
'web', | ||
'githubUrl', | ||
'twitterUrl', | ||
'facebookUrl', | ||
]; | ||
/** A singleton attached to the class that stores it's instances to enable convergence of data */ | ||
static fellows = []; | ||
// ----------------------------------- | ||
// Methods | ||
// Username and Name | ||
/** GitHub Username */ | ||
githubUsername = ''; | ||
/** GitLab Username */ | ||
gitlabUsername = ''; | ||
/** Twitter Username */ | ||
twitterUsername = ''; | ||
/** Facebook Username */ | ||
facebookUsername = ''; | ||
/** OpenCollective Username */ | ||
opencollectiveUsername = ''; | ||
/** Patreon Username */ | ||
patreonUsername = ''; | ||
/** Fields used to resolve {@link Fellow.username} */ | ||
usernameFields = [ | ||
'githubUsername', | ||
'gitlabUsername', | ||
'twitterUsername', | ||
'facebookUsername', | ||
'opencollectiveUsername', | ||
'patreonUsername', | ||
]; | ||
/** Get all unique resolved usernames */ | ||
get usernames() { | ||
return this.getFields(this.usernameFields); | ||
} | ||
/** Get the first resolved {@link Fellow.usernameFields} that is truthy. */ | ||
get username() { | ||
return this.getFirstField(this.usernameFields) || ''; | ||
} | ||
/** Years active for the current repository, extracted from the name */ | ||
years = ''; | ||
/** Storage of the Nomen (e.g. `Ben`, or `Benjamin Lupton`, but not `balupton`) */ | ||
_nomen = ''; | ||
/** Get the resolved Nomen */ | ||
get nomen() { | ||
// clear if not actually a nomen | ||
if (this.usernames.includes(this._nomen)) { | ||
this._nomen = ''; | ||
} | ||
// return | ||
return this._nomen; | ||
} | ||
/** | ||
* Sort a list of fellows. | ||
* Uses {@link Fellow::sort} for the comparison. | ||
* If the input is prefixed with a series of numbers, that is considered the year: | ||
* E.g. Given `2015+ Bevry Pty Ltd` then `2015+` is the years | ||
* E.g. Given `2013-2015 Bevry Pty Ltd` then `2013-2015` is the years | ||
*/ | ||
static sort(list) { | ||
return list.sort(comparator); | ||
set nomen(input) { | ||
const match = /^((?:[0-9]+[-+]?)+)?(.+)$/.exec(input); | ||
if (match) { | ||
// fetch the years, but for now, discard it | ||
const years = String(match[1] || '').trim(); | ||
if (years) | ||
this.years = years; | ||
// fetch the name | ||
const name = trim(match[2]); | ||
// apply if actually a nomen | ||
if (this.usernames.includes(name) === false) | ||
this._nomen = name; | ||
} | ||
} | ||
/** Flatten lists of fellows into one set of fellows */ | ||
static flatten(lists) { | ||
const fellows = new Set(); | ||
for (const list of lists) { | ||
for (const fellow of list) { | ||
fellows.add(fellow); | ||
} | ||
/** Get {@link Follow.nomen} if resolved, otherwise {@link Fellow.username} */ | ||
get name() { | ||
return this.nomen || this.username || ''; | ||
} | ||
/** Alias for {@link Fellow.nomen} */ | ||
set name(input) { | ||
this.nomen = input; | ||
} | ||
// ----------------------------------- | ||
// URLs | ||
/** Storage of the Website URL */ | ||
_websiteUrl = ''; | ||
/** Get the resolved Website URL. Used by GitHub GraphQL API. */ | ||
get websiteUrl() { | ||
return this._websiteUrl; | ||
} | ||
/** Alias for {@link Fellow.url} */ | ||
set websiteUrl(input) { | ||
this.url = input; | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by npm. Used by prior Fellow versions. */ | ||
get homepage() { | ||
return this.websiteUrl; | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by npm. Used by prior Fellow versions. */ | ||
set homepage(input) { | ||
this.websiteUrl = input; | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub GraphQL API. */ | ||
get blog() { | ||
return this.websiteUrl; | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub REST API. */ | ||
set blog(input) { | ||
this.websiteUrl = input; | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub GraphQL API. */ | ||
/* eslint-disable-next-line camelcase */ | ||
get html_url() { | ||
return this.websiteUrl; | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub REST API. */ | ||
/* eslint-disable-next-line camelcase */ | ||
set html_url(input) { | ||
this.websiteUrl = input; | ||
} | ||
/** Get the GitHub URL from the {@link Fellow.githubUsername} */ | ||
get githubUrl() { | ||
return this.githubUsername | ||
? `https://github.com/${this.githubUsername}` | ||
: ''; | ||
} | ||
/** Set the GitHub URL and username from an input */ | ||
set githubUrl(input) { | ||
const username = getUsernameFromGitHubSponsorsUrl(input) || | ||
getUsernameFromGitHubUrl(input) || | ||
getUsernameFromGistUrl(input) || | ||
getGitHubUsernameFromThanksDevUrl(input); | ||
if (username) { | ||
this.githubUsername = username; | ||
} | ||
return fellows; | ||
else if (input.includes('github.com')) { | ||
// it is probably something like: https://github.com/apps/dependabot | ||
// ignore as it is not a person | ||
} | ||
else { | ||
throw new Error(`Invalid GitHub URL: ${input}`); | ||
} | ||
} | ||
/** Compare to another fellow for sorting. */ | ||
compare(other) { | ||
const A = this.name.toLowerCase(); | ||
const B = other.name.toLowerCase(); | ||
if (A === B) { | ||
return 0; | ||
/** Get the GitLab URL from the {@link Fellow.gitlabUsername} */ | ||
get gitlabUrl() { | ||
return this.gitlabUsername | ||
? `https://gitlab.com/${this.gitlabUsername}` | ||
: ''; | ||
} | ||
/** Set the GitLab URL and username from an input */ | ||
set gitlabUrl(input) { | ||
const username = getUsernameFromGitLabUrl(input) || | ||
getGitLabUsernameFromThanksDevUrl(input); | ||
if (username) { | ||
this.gitlabUsername = username; | ||
} | ||
else if (A < B) { | ||
return -1; | ||
else { | ||
throw new Error(`Invalid GitLab URL: ${input}`); | ||
} | ||
} | ||
/** Get the Facebook URL from the {@link Fellow.twitterUsername} */ | ||
get twitterUrl() { | ||
return this.twitterUsername | ||
? `https://twitter.com/${this.twitterUsername}` | ||
: ''; | ||
} | ||
/** Set the Twitter URL and username from an input */ | ||
set twitterUrl(input) { | ||
const username = getUsernameFromTwitterUrl(input); | ||
if (username) { | ||
this.twitterUsername = username; | ||
} | ||
else { | ||
return 1; | ||
throw new Error(`Invalid Twitter URL: ${input}`); | ||
} | ||
} | ||
/** | ||
* Compare to another fellow for equivalancy. | ||
* Uses {@link Fellow::idFields} for the comparison. | ||
* @param other The other fellow to compare ourselves with | ||
* @returns Returns `true` if they appear to be the same person, or `false` if not. | ||
*/ | ||
same(other) { | ||
for (const field of this.idFields) { | ||
const value = this[field]; | ||
const otherValue = other[field]; | ||
if (value && otherValue) { | ||
if (value instanceof Set && otherValue instanceof Set) { | ||
for (const item of value) { | ||
if (otherValue.has(item)) { | ||
return true; | ||
} | ||
} | ||
/** Get the Facebook URL from the {@link Fellow.facebookUsername} */ | ||
get facebookUrl() { | ||
return this.facebookUsername | ||
? `https://facebook.com/${this.facebookUsername}` | ||
: ''; | ||
} | ||
/** Set the Facebook URL and username from an input */ | ||
set facebookUrl(input) { | ||
const username = getUsernameFromFacebookUrl(input); | ||
if (username) { | ||
this.facebookUsername = username; | ||
} | ||
else { | ||
throw new Error(`Invalid Facebook URL: ${input}`); | ||
} | ||
} | ||
/** Get the Patreon URL from the {@link Fellow.patreonUsername} */ | ||
get patreonUrl() { | ||
return this.patreonUsername | ||
? `https://patreon.com/${this.patreonUsername}` | ||
: ''; | ||
} | ||
/** Set the Patreon URL and username from an input */ | ||
set patreonUrl(input) { | ||
const username = getUsernameFromPatreonUrl(input); | ||
if (username) { | ||
this.patreonUsername = username; | ||
} | ||
else { | ||
throw new Error(`Invalid Patreon URL: ${input}`); | ||
} | ||
} | ||
/** Get the OpenCollective URL from the {@link Fellow.opencollectiveUsername} */ | ||
get opencollectiveUrl() { | ||
return this.opencollectiveUsername | ||
? `https://opencollective.com/${this.opencollectiveUsername}` | ||
: ''; | ||
} | ||
/** Set the OpenCollective URL and username from an input */ | ||
set opencollectiveUrl(input) { | ||
const username = getUsernameFromOpenCollectiveUrl(input); | ||
if (username) { | ||
this.opencollectiveUsername = username; | ||
} | ||
else { | ||
throw new Error(`Invalid OpenCollective URL: ${input}`); | ||
} | ||
} | ||
/** Get the ThanksDev URL from the {@link Fellow.githubUsername} or {@link Fellow.gitlabUsername} */ | ||
get thanksdevUrl() { | ||
return this.githubUsername | ||
? `https://thanks.dev/d/gh/${this.githubUsername}` | ||
: this.gitlabUsername | ||
? `https://thanks.dev/d/gl/${this.gitlabUsername}` | ||
: ''; | ||
} | ||
/** Set the ThanksDev URL and username from an input */ | ||
set thanksdevUrl(input) { | ||
const githubUsername = getGitHubUsernameFromThanksDevUrl(input); | ||
if (githubUsername) { | ||
this.githubUsername = githubUsername; | ||
} | ||
else { | ||
const gitlabUsername = getGitLabUsernameFromThanksDevUrl(input); | ||
if (gitlabUsername) { | ||
this.gitlabUsername = gitlabUsername; | ||
} | ||
else { | ||
throw new Error(`Invalid ThanksDev URL: ${input}`); | ||
} | ||
} | ||
} | ||
/** URL fields used to resolve {@link Fellow.url} */ | ||
urlFields = [ | ||
'websiteUrl', | ||
'githubUrl', | ||
'gitlabUrl', | ||
'twitterUrl', | ||
'facebookUrl', | ||
'patreonUrl', | ||
'opencollectiveUrl', | ||
'thanksdevUrl', | ||
]; | ||
/** Get all unique resolved URLs */ | ||
get urls() { | ||
return this.getFields(this.urlFields); | ||
} | ||
/** Get the first resolved {@link Fellow.urlFields}. Used by GitHub GraphQL API. */ | ||
get url() { | ||
return this.getFirstField(this.urlFields) || ''; | ||
} | ||
/** Set the appropriate {@link Fellow.urlFields} from the input */ | ||
set url(input) { | ||
input = trim(input); | ||
if (input) { | ||
// convert to https | ||
input = input.replace(/^http:\/\//, 'https://'); | ||
// slice 1 to skip websiteUrl | ||
for (const field of this.urlFields) { | ||
// skip websiteUrl in any order, as that is our fallback | ||
if (field === 'websiteUrl') | ||
continue; | ||
// attempt application of the field | ||
try { | ||
this[field] = input; | ||
// the application was successful, it is not a websiteUrl | ||
return; | ||
} | ||
else if (Array.isArray(value) && Array.isArray(otherValue)) { | ||
for (const item of value) { | ||
if (otherValue.includes(item)) { | ||
return true; | ||
} | ||
} | ||
catch (err) { | ||
// the application failed, try the next field | ||
continue; | ||
} | ||
else if (value === otherValue) { | ||
return true; | ||
} | ||
} | ||
// all non-websiteUrl applications failed, it must be a websiteUrl | ||
this._websiteUrl = input; | ||
} | ||
return false; | ||
} | ||
/** | ||
* With the value, see if an existing fellow exists in our singleton list property with the value, otherwise create a new fellow instance with the value and add them to our singleton list. | ||
* Uses {@link Fellow::same} for the comparison. | ||
* @param value The value to create a new fellow instance or find the existing fellow instance with | ||
* @param add Whether to add the created person to the list | ||
* @returns The new or existing fellow instance | ||
*/ | ||
static ensure(value, add = true) { | ||
const newFellow = this.create(value); | ||
for (const existingFellow of this.fellows) { | ||
if (newFellow.same(existingFellow)) { | ||
return existingFellow.set(value); | ||
} | ||
// ----------------------------------- | ||
// Emails | ||
/** Emails used */ | ||
emails = new Set(); | ||
/** Fetch the first email that was applied, otherwise an empty string */ | ||
get email() { | ||
for (const email of this.emails) { | ||
return email; | ||
} | ||
if (add) { | ||
this.fellows.push(newFellow); | ||
return newFellow; | ||
return ''; | ||
} | ||
/** Add the email to the set instead of replacing it */ | ||
set email(input) { | ||
input = trim(input); | ||
if (input) { | ||
this.emails.add(input); | ||
} | ||
else { | ||
throw new Error(`Fellow by ${value} does not exist`); | ||
} | ||
} | ||
/** | ||
* Get a fellow from the singleton list | ||
* @param value The value to fetch the value with | ||
* @returns The fetched fellow, if they exist with that value | ||
*/ | ||
static get(value) { | ||
return this.ensure(value, false); | ||
// ----------------------------------- | ||
// Description | ||
/** Storage of the description */ | ||
_description = ''; | ||
/** Get the resolved description */ | ||
get description() { | ||
return this._description; | ||
} | ||
/** Set the resolved description */ | ||
set description(input) { | ||
input = trim(input); | ||
this._description = input; | ||
} | ||
/** Alias for {@link Fellow.description} */ | ||
get bio() { | ||
return this.description; | ||
} | ||
/** Alias for {@link Fellow.description} */ | ||
set bio(input) { | ||
this.description = input; | ||
} | ||
// ----------------------------------- | ||
// Identification | ||
/** | ||
* Add a fellow or a series of people, denoted by the value, to the singleton list | ||
* @param values The fellow or people to add | ||
* @returns An array of the fellow objects for the passed people | ||
* An array of field names that are used to determine if two fellow's are the same. | ||
* Don't need to add usernames for github, twitter, and facebook, as they will be compared via `urls`. | ||
* Can't compare just username, as that is not unique unless comparison's are on the same service, hence why `urls` are used and not `usernames`. | ||
*/ | ||
static add(...values) { | ||
const result = []; | ||
for (const value of values) { | ||
if (value instanceof this) { | ||
result.push(this.ensure(value)); | ||
idFields = ['urls', 'emails']; | ||
/** An array of identifiers, all lowercased to prevent typestrong/TypeStrong double-ups */ | ||
get ids() { | ||
const results = new Set(); | ||
for (const field of this.idFields) { | ||
const value = this[field]; | ||
if (value instanceof Set || Array.isArray(value)) { | ||
for (const item of value) { | ||
results.add(item.toLowerCase()); | ||
} | ||
} | ||
else if (typeof value === 'string') { | ||
result.push(...value.split(/, +/).map((fellow) => this.ensure(fellow))); | ||
else { | ||
return String(value).toLowerCase(); | ||
} | ||
else if (Array.isArray(value)) { | ||
result.push(...value.map((value) => this.ensure(value))); | ||
} | ||
else if (value) { | ||
result.push(this.ensure(value)); | ||
} | ||
} | ||
return result; | ||
return Array.from(results.values()).filter(Boolean); | ||
} | ||
/** Create a new Fellow instance with the value, however if the value is already a fellow instance, then just return it */ | ||
static create(value) { | ||
return value instanceof this ? value : new this(value); | ||
} | ||
// ----------------------------------- | ||
// Methods | ||
/** | ||
* Construct our fellow instance with the value | ||
* @param value The value used to set the properties of the fellow, forwarded to {@link Fellow::set} | ||
* @param input The value used to set the properties of the fellow, forwarded to {@link Fellow.set} | ||
*/ | ||
constructor(value) { | ||
this.set(value); | ||
constructor(input) { | ||
this.set(input); | ||
} | ||
@@ -184,5 +433,5 @@ /** | ||
} | ||
const name = (match[1] || '').trim(); | ||
const email = (match[2] || '').trim(); | ||
const url = (match[3] || '').trim(); | ||
const name = trim(match[1]); | ||
const email = trim(match[2]); | ||
const url = trim(match[3]); | ||
if (name) | ||
@@ -200,111 +449,186 @@ this.name = name; | ||
return; // skip if private | ||
const value = fellow[key] || null; | ||
if (value) { | ||
// if any of the url fields, redirect to url setter | ||
if (this.urlFields.includes(key)) { | ||
this.url = value; | ||
} | ||
// if not a url field, e.g. name or email | ||
else { | ||
this[key] = value; | ||
} | ||
} | ||
const value = trim(fellow[key]); | ||
if (value) | ||
this[key] = value; | ||
}); | ||
} | ||
else { | ||
throw new Error('Invalid fellow input'); | ||
throw new Error(`Invalid Fellow input: ${JSON.stringify(fellow)}`); | ||
} | ||
return this; | ||
} | ||
// ----------------------------------- | ||
// Accessors | ||
/** Compare to another fellow for sorting. */ | ||
compare(other) { | ||
const a = this.name.toLowerCase(); | ||
const b = other.name.toLowerCase(); | ||
if (a === b) { | ||
return 0; | ||
} | ||
else if (a < b) { | ||
return -1; | ||
} | ||
else { | ||
return 1; | ||
} | ||
} | ||
/** | ||
* If the name is empty, we will try to fallback to githubUsername then twitterUsername | ||
* If the name is prefixed with a series of numbers, that is considered the year | ||
* E.g. In `2015+ Bevry Pty Ltd` then `2015+` is the years | ||
* E.g. In `2013-2015 Bevry Pty Ltd` then `2013-2015` is the years | ||
* Compare to another fellow for equivalency. | ||
* Uses {@link Fellow.ids} for the comparison. | ||
* @param other The other fellow to compare ourselves with | ||
* @returns Returns `true` if they appear to be the same person, or `false` if not. | ||
*/ | ||
set name(value /* :string */) { | ||
const match = /^((?:[0-9]+[-+]?)+)?(.+)$/.exec(value); | ||
if (match) { | ||
// fetch the years, but for now, discard it | ||
const years = String(match[1] || '').trim(); | ||
if (years) | ||
this.years = years; | ||
// fetch the name, and apply it | ||
const name = match[2].trim(); | ||
if (name) | ||
this._name = name; | ||
same(other) { | ||
const ids = new Set(this.ids); | ||
const otherIds = new Set(other.ids); | ||
for (const id of ids) { | ||
if (otherIds.has(id)) { | ||
return true; | ||
} | ||
} | ||
return false; | ||
} | ||
// ----------------------------------- | ||
// Static | ||
/** | ||
* Fetch the user's name, otherwise their usernames | ||
* Sort a list of fellows. | ||
* Uses {@link Fellow.compare} for the comparison. | ||
*/ | ||
get name() { | ||
return (this._name || | ||
this.githubUsername || | ||
this.twitterUsername || | ||
this.facebookUsername || | ||
''); | ||
static sort(list) { | ||
if (list instanceof Set) { | ||
list = Array.from(list.values()); | ||
} | ||
list = list.sort(comparator); | ||
return list; | ||
} | ||
/** Add the email to the set instead of replacing it */ | ||
set email(value) { | ||
if (value) { | ||
this.emails.add(value); | ||
/** Flatten lists of fellows into one set of fellows */ | ||
static flatten(lists) { | ||
const fellows = new Set(); | ||
for (const list of lists) { | ||
for (const fellow of list) { | ||
fellows.add(fellow); | ||
} | ||
} | ||
return fellows; | ||
} | ||
/** Fetch the first email that was applied, otherwise an empty string */ | ||
get email() { | ||
for (const email of this.emails) { | ||
return email; | ||
/** | ||
* With the value, see if an existing fellow exists in our singleton list property with the value, otherwise create a new fellow instance with the value and add them to our singleton list. | ||
* Uses {@link Fellow.same} for the comparison. | ||
* @param input The value to create a new fellow instance or find the existing fellow instance with | ||
* @param add Whether to add the created person to the list | ||
* @returns The new or existing fellow instance | ||
*/ | ||
static ensure(input, add = true) { | ||
if (input instanceof Fellow && this.fellows.includes(input)) | ||
return input; | ||
const newFellow = this.create(input); | ||
for (const existingFellow of this.fellows) { | ||
if (newFellow.same(existingFellow)) { | ||
return existingFellow.set(input); | ||
} | ||
} | ||
return ''; | ||
if (add) { | ||
this.fellows.push(newFellow); | ||
return newFellow; | ||
} | ||
else { | ||
throw new Error(`Fellow does not exist: ${input}`); | ||
} | ||
} | ||
/** | ||
* Will determine if the passed URL is a github, facebook, or twitter URL. | ||
* If it is, then it will extract the username and url from it. | ||
* If it was not, then it will set the homepage variable. | ||
* Get a fellow from the singleton list | ||
* @param input The value to fetch the value with | ||
* @returns The fetched fellow, if they exist with that value | ||
*/ | ||
set url(input) { | ||
if (input) { | ||
let url; | ||
// github | ||
const githubMatch = /^.+github.com\/([^/]+)\/?$/.exec(input); | ||
if (githubMatch) { | ||
this.githubUsername = githubMatch[1]; | ||
url = this.githubUrl = 'https://github.com/' + this.githubUsername; | ||
static get(input) { | ||
return this.ensure(input, false); | ||
} | ||
/** | ||
* Add a fellow or a series of people, denoted by the value, to the singleton list | ||
* @param inputs The fellow or people to add | ||
* @returns A de-duplicated array of fellow objects for the passed people | ||
*/ | ||
static add(...inputs) { | ||
const list = new Set(); | ||
for (const input of inputs) { | ||
if (input instanceof this) { | ||
list.add(this.ensure(input)); | ||
} | ||
else { | ||
const facebookMatch = /^.+facebook.com\/([^/]+)\/?$/.exec(input); | ||
if (facebookMatch) { | ||
this.facebookUsername = facebookMatch[1]; | ||
url = this.facebookUrl = | ||
'https://facebook.com/' + this.facebookUsername; | ||
else if (typeof input === 'string') { | ||
for (const item of input.split(/, +/)) { | ||
list.add(this.ensure(item)); | ||
} | ||
else { | ||
const twitterMatch = /^.+twitter.com\/([^/]+)\/?$/.exec(input); | ||
if (twitterMatch) { | ||
this.twitterUsername = twitterMatch[1]; | ||
url = this.twitterUrl = | ||
'https://twitter.com/' + this.twitterUsername; | ||
} | ||
else { | ||
url = this.homepage = input; | ||
} | ||
} | ||
else if (input instanceof Set || Array.isArray(input)) { | ||
for (const item of input) { | ||
list.add(this.ensure(item)); | ||
} | ||
} | ||
// add url in encrypted and unecrypted forms to urls | ||
this.urls.add(url.replace(/^http:/, 'https:')); | ||
this.urls.add(url.replace(/^https:/, 'http:')); | ||
else if (input) { | ||
list.add(this.ensure(input)); | ||
} | ||
} | ||
return Array.from(list.values()); | ||
} | ||
/** Fetch the homepage with fallback to one of the service URLs if available */ | ||
get url() { | ||
return (this.homepage || | ||
this.githubUrl || | ||
this.facebookUrl || | ||
this.twitterUrl || | ||
''); | ||
/** Create a new Fellow instance with the value, however if the value is already a fellow instance, then just return it */ | ||
static create(value) { | ||
return value instanceof this ? value : new this(value); | ||
} | ||
/** Get the field field from the list that isn't empty */ | ||
// ----------------------------------- | ||
// Repositories | ||
/** Set of GitHub repository slugs that the fellow authors */ | ||
authorOfRepositories = new Set(); | ||
/** Get all fellows who author a particular GitHub repository */ | ||
static authorsOfRepository(repoSlug) { | ||
return this.sort(this.fellows.filter(function (fellow) { | ||
return fellow.authorOfRepositories.has(repoSlug); | ||
})); | ||
} | ||
/** Set of GitHub repository slugs that the fellow maintains */ | ||
maintainerOfRepositories = new Set(); | ||
/** Get all fellows who maintain a particular GitHub repository */ | ||
static maintainersOfRepository(repoSlug) { | ||
return this.sort(this.fellows.filter(function (fellow) { | ||
return fellow.maintainerOfRepositories.has(repoSlug); | ||
})); | ||
} | ||
/** Map of GitHub repository slugs to the contribution count of the user */ | ||
contributionsOfRepository = new Map(); | ||
/** Set of GitHub repository slugs that the fellow contributes to */ | ||
contributorOfRepositories = new Set(); | ||
/** Get all fellows who contribute to a particular GitHub repository */ | ||
static contributorsOfRepository(repoSlug) { | ||
return this.sort(this.fellows.filter(function (fellow) { | ||
return fellow.contributorOfRepositories.has(repoSlug); | ||
})); | ||
} | ||
/** Set of GitHub repository slugs that the fellow initially financed */ | ||
funderOfRepositories = new Set(); | ||
/** Get all fellows who initally financed a particular GitHub repository */ | ||
static fundersOfRepository(repoSlug) { | ||
return this.sort(this.fellows.filter(function (fellow) { | ||
return fellow.funderOfRepositories.has(repoSlug); | ||
})); | ||
} | ||
/** Set of GitHub repository slugs that the fellow actively finances */ | ||
sponsorOfRepositories = new Set(); | ||
/** Get all fellows who actively finance a particular GitHub repository */ | ||
static sponsorsOfRepository(repoSlug) { | ||
return this.sort(this.fellows.filter(function (fellow) { | ||
return fellow.sponsorOfRepositories.has(repoSlug); | ||
})); | ||
} | ||
/** Set of GitHub repository slugs that the fellow has historically financed */ | ||
donorOfRepositories = new Set(); | ||
/** Get all fellows who have historically financed a particular GitHub repository */ | ||
static donorsOfRepository(repoSlug) { | ||
return this.sort(this.fellows.filter(function (fellow) { | ||
return fellow.donorOfRepositories.has(repoSlug); | ||
})); | ||
} | ||
// @todo figure out how to calculate this | ||
// /** Map of GitHub repository slugs to the sponsorship amount of the user */ | ||
// readonly sponsorships = new Map<string, number>() | ||
// ----------------------------------- | ||
// Formatting | ||
/** Get the first field from the list that isn't empty */ | ||
getFirstField(fields) { | ||
@@ -318,30 +642,12 @@ for (const field of fields) { | ||
} | ||
// ----------------------------------- | ||
// Repositories | ||
/** Get all fellows who administrate a particular repository */ | ||
static administersRepository(repoSlug) { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.administeredRepositories.has(repoSlug); | ||
}); | ||
/** Get the all the de-duplicated fields from the list that aren't empty */ | ||
getFields(fields) { | ||
const set = new Set(); | ||
for (const field of fields) { | ||
const value = this[field]; | ||
if (value) | ||
set.add(value); | ||
} | ||
return Array.from(set.values()); | ||
} | ||
/** Get all fellows who contribute to a particular repository */ | ||
static contributesRepository(repoSlug) { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.contributedRepositories.has(repoSlug); | ||
}); | ||
} | ||
/** Get all fellows who maintain a particular repository */ | ||
static maintainsRepository(repoSlug) { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.maintainedRepositories.has(repoSlug); | ||
}); | ||
} | ||
/** Get all fellows who author a particular repository */ | ||
static authorsRepository(repoSlug) { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.authoredRepositories.has(repoSlug); | ||
}); | ||
} | ||
// ----------------------------------- | ||
// Formats | ||
/** | ||
@@ -363,3 +669,3 @@ * Convert the fellow into the usual string format | ||
if (format.displayEmail && this.email) { | ||
if (format.displayEmail !== false && this.email) { | ||
parts.push(`<${this.email}>`); | ||
@@ -378,2 +684,30 @@ } | ||
/** | ||
* Convert the fellow into the usual text format | ||
* @example `NAME 📝 DESCRIPTION 🔗 URL` | ||
*/ | ||
toText(format = {}) { | ||
if (!this.name) | ||
return ''; | ||
const parts = []; | ||
// prefix | ||
if (format.prefix) | ||
parts.push(format.prefix); | ||
// name | ||
parts.push(`${this.name}`); | ||
if (format.displayEmail && this.email) { | ||
parts.push(`✉️ ${this.email}`); | ||
} | ||
// description | ||
if (format.displayDescription !== false && this.description) { | ||
parts.push(`📝 ${this.description}`); | ||
} | ||
// url | ||
if (format.displayUrl !== false && this.url) { | ||
parts.push(`🔗 ${this.url}`); | ||
} | ||
// return | ||
return parts.join(' '); | ||
} | ||
/** | ||
* Convert the fellow into the usual markdown format | ||
@@ -407,4 +741,4 @@ * @example `[NAME](URL) <EMAIL>` | ||
this.githubUsername) { | ||
const contributionsURL = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}`; | ||
parts.push(`— [view contributions](${contributionsURL} "View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}")`); | ||
const contributionsUrl = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}`; | ||
parts.push(`— [view contributions](${contributionsUrl} "View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}")`); | ||
} | ||
@@ -442,4 +776,4 @@ // return | ||
this.githubUsername) { | ||
const contributionsURL = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}`; | ||
parts.push(`— <a href="${contributionsURL}" title="View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}">view contributions</a>`); | ||
const contributionsUrl = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}`; | ||
parts.push(`— <a href="${contributionsUrl}" title="View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}">view contributions</a>`); | ||
} | ||
@@ -446,0 +780,0 @@ // return |
@@ -0,21 +1,103 @@ | ||
/** | ||
* Options for formatting the rendered outputs. | ||
* Defaults differ for each output. | ||
* Not all options are relevant on all outputs. | ||
*/ | ||
export interface FormatOptions { | ||
/** Whether or not to display the fellow's email */ | ||
displayEmail?: true | ||
/** A string to proceed each entry */ | ||
prefix?: string | ||
/** Whether or not to display {@link Fellow.url} */ | ||
displayUrl?: boolean | ||
/** Whether or not to display {@link Fellow.description} */ | ||
displayDescription?: boolean | ||
/** Whether or not to display {@link Fellow.email} */ | ||
displayEmail?: boolean | ||
/** Whether or not to display the copright icon */ | ||
displayCopyright?: boolean | ||
/** Whether or not to display the copyright years */ | ||
/** Whether or not to display {@link Fellow.years} */ | ||
displayYears?: boolean | ||
/** Whether or not to display a link to the user's contributions, if used along with {@link .githubRepoSlug} */ | ||
/** Whether or not to display a link to the user's contributions, if used along with {@link FormatOptions.githubRepoSlug} */ | ||
displayContributions?: boolean | ||
/** The repository for when using with {@link .displayContributions} */ | ||
/** The repository for when using with {@link FormatOptions.displayContributions} */ | ||
githubRepoSlug?: string | ||
/** An array of fields to prefer for the URL */ | ||
urlFields?: ['githubUrl', 'url'] | ||
urlFields?: Array<string> | ||
} | ||
/** GitHub Sponsors URL */ | ||
function getUsernameFromGitHubSponsorsUrl(url: string): string { | ||
const match = /^https?:\/\/github\.com\/sponsors\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** GitHub URL */ | ||
function getUsernameFromGitHubUrl(url: string): string { | ||
const match = /^https?:\/\/github\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** Gist URL */ | ||
function getUsernameFromGistUrl(url: string): string { | ||
const match = /^https?:\/\/gist\.github\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** GitLab URL */ | ||
function getUsernameFromGitLabUrl(url: string): string { | ||
const match = /^https?:\/\/gitlab\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** ThanksDev GitHub URL */ | ||
function getGitHubUsernameFromThanksDevUrl(url: string): string { | ||
const match = /^https?:\/\/thanks\.dev\/d\/gh\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** ThanksDev GitLab URL */ | ||
function getGitLabUsernameFromThanksDevUrl(url: string): string { | ||
const match = /^https?:\/\/thanks\.dev\/d\/gl\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** Facebook URL */ | ||
function getUsernameFromFacebookUrl(url: string): string { | ||
const match = /^https?:\/\/facebook\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** Twitter URL */ | ||
function getUsernameFromTwitterUrl(url: string): string { | ||
const match = /^https?:\/\/twitter\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** Patreon URL */ | ||
function getUsernameFromPatreonUrl(url: string): string { | ||
const match = /^https?:\/\/patreon\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** OpenCollective URL */ | ||
function getUsernameFromOpenCollectiveUrl(url: string): string { | ||
const match = /^https?:\/\/opencollective\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** Trim a value if it is a string */ | ||
function trim(value: any): typeof value { | ||
if (typeof value === 'string') { | ||
return value.trim() | ||
} | ||
return value | ||
} | ||
/** Comparator for sorting fellows in an array */ | ||
@@ -34,179 +116,381 @@ export function comparator(a: Fellow, b: Fellow) { | ||
/** Actual name that is stored, otherwise falls back to username from the url fields */ | ||
private _name?: string | ||
/** A singleton attached to the class that stores it's instances to enable convergence of data */ | ||
static readonly fellows: Array<Fellow> = [] | ||
/** Years active for the current repository, extracted from the name */ | ||
public years?: string | ||
// ----------------------------------- | ||
// Username and Name | ||
/** URLs used */ | ||
readonly urls = new Set<string>() | ||
/** GitHub Username */ | ||
githubUsername: string = '' | ||
/** Emails used */ | ||
readonly emails = new Set<string>() | ||
/** GitLab Username */ | ||
gitlabUsername: string = '' | ||
/** Map of repository slugs with the contributions from the user */ | ||
readonly contributions = new Map<string, number>() | ||
/** Twitter Username */ | ||
twitterUsername: string = '' | ||
/** Set of repository slugs that the fellow administers to */ | ||
readonly administeredRepositories = new Set<string>() | ||
/** Facebook Username */ | ||
facebookUsername: string = '' | ||
/** Set of repository slugs that the fellow contributes to */ | ||
readonly contributedRepositories = new Set<string>() | ||
/** OpenCollective Username */ | ||
opencollectiveUsername: string = '' | ||
/** Set of repository slugs that the fellow maintains */ | ||
readonly maintainedRepositories = new Set<string>() | ||
/** Patreon Username */ | ||
patreonUsername: string = '' | ||
/** Set of repository slugs that the fellow authors */ | ||
readonly authoredRepositories = new Set<string>() | ||
/** Fields used to resolve {@link Fellow.username} */ | ||
protected readonly usernameFields = [ | ||
'githubUsername', | ||
'gitlabUsername', | ||
'twitterUsername', | ||
'facebookUsername', | ||
'opencollectiveUsername', | ||
'patreonUsername', | ||
] | ||
/** Get all unique resolved usernames */ | ||
get usernames(): Array<string> { | ||
return this.getFields(this.usernameFields) | ||
} | ||
/** Get the first resolved {@link Fellow.usernameFields} that is truthy. */ | ||
get username() { | ||
return this.getFirstField(this.usernameFields) || '' | ||
} | ||
/** Years active for the current repository, extracted from the name */ | ||
public years: string = '' | ||
/** Storage of the Nomen (e.g. `Ben`, or `Benjamin Lupton`, but not `balupton`) */ | ||
private _nomen: string = '' | ||
/** Get the resolved Nomen */ | ||
get nomen() { | ||
// clear if not actually a nomen | ||
if (this.usernames.includes(this._nomen)) { | ||
this._nomen = '' | ||
} | ||
// return | ||
return this._nomen | ||
} | ||
/** | ||
* An array of field names that are used to determine if two fellow's are the same. | ||
* Don't need to add usernames for github, twitter, and facebook, as they will be compared via `urls`. | ||
* Can't compare just username, as that is not unique unless comparison's are on the same service, hence why `urls` are used and not `usernames`. | ||
* If the input is prefixed with a series of numbers, that is considered the year: | ||
* E.g. Given `2015+ Bevry Pty Ltd` then `2015+` is the years | ||
* E.g. Given `2013-2015 Bevry Pty Ltd` then `2013-2015` is the years | ||
*/ | ||
protected readonly idFields = ['urls', 'emails'] | ||
set nomen(input: string) { | ||
const match = /^((?:[0-9]+[-+]?)+)?(.+)$/.exec(input) | ||
if (match) { | ||
// fetch the years, but for now, discard it | ||
const years = String(match[1] || '').trim() | ||
if (years) this.years = years | ||
// fetch the name | ||
const name = trim(match[2]) | ||
// apply if actually a nomen | ||
if (this.usernames.includes(name) === false) this._nomen = name | ||
} | ||
} | ||
/** An array of field names that are used to determine the fellow's URL */ | ||
protected readonly urlFields = [ | ||
'url', | ||
'homepage', | ||
'web', | ||
'githubUrl', | ||
'twitterUrl', | ||
'facebookUrl', | ||
] | ||
/** Get {@link Follow.nomen} if resolved, otherwise {@link Fellow.username} */ | ||
get name(): string { | ||
return this.nomen || this.username || '' | ||
} | ||
/** Alias for {@link Fellow.nomen} */ | ||
set name(input: string) { | ||
this.nomen = input | ||
} | ||
/** A singleton attached to the class that stores it's instances to enable convergence of data */ | ||
static readonly fellows: Array<Fellow> = [] | ||
// ----------------------------------- | ||
// Methods | ||
// URLs | ||
/** | ||
* Sort a list of fellows. | ||
* Uses {@link Fellow::sort} for the comparison. | ||
*/ | ||
static sort(list: Array<Fellow>) { | ||
return list.sort(comparator) | ||
/** Storage of the Website URL */ | ||
private _websiteUrl: string = '' | ||
/** Get the resolved Website URL. Used by GitHub GraphQL API. */ | ||
get websiteUrl() { | ||
return this._websiteUrl | ||
} | ||
/** Alias for {@link Fellow.url} */ | ||
set websiteUrl(input: string) { | ||
this.url = input | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by npm. Used by prior Fellow versions. */ | ||
get homepage() { | ||
return this.websiteUrl | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by npm. Used by prior Fellow versions. */ | ||
set homepage(input: string) { | ||
this.websiteUrl = input | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub GraphQL API. */ | ||
get blog() { | ||
return this.websiteUrl | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub REST API. */ | ||
set blog(input: string) { | ||
this.websiteUrl = input | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub GraphQL API. */ | ||
/* eslint-disable-next-line camelcase */ | ||
get html_url() { | ||
return this.websiteUrl | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub REST API. */ | ||
/* eslint-disable-next-line camelcase */ | ||
set html_url(input: string) { | ||
this.websiteUrl = input | ||
} | ||
/** Flatten lists of fellows into one set of fellows */ | ||
static flatten(lists: Array<Array<Fellow> | Set<Fellow>>): Set<Fellow> { | ||
const fellows = new Set<Fellow>() | ||
for (const list of lists) { | ||
for (const fellow of list) { | ||
fellows.add(fellow) | ||
} | ||
/** Get the GitHub URL from the {@link Fellow.githubUsername} */ | ||
get githubUrl() { | ||
return this.githubUsername | ||
? `https://github.com/${this.githubUsername}` | ||
: '' | ||
} | ||
/** Set the GitHub URL and username from an input */ | ||
set githubUrl(input: string) { | ||
const username = | ||
getUsernameFromGitHubSponsorsUrl(input) || | ||
getUsernameFromGitHubUrl(input) || | ||
getUsernameFromGistUrl(input) || | ||
getGitHubUsernameFromThanksDevUrl(input) | ||
if (username) { | ||
this.githubUsername = username | ||
} else if (input.includes('github.com')) { | ||
// it is probably something like: https://github.com/apps/dependabot | ||
// ignore as it is not a person | ||
} else { | ||
throw new Error(`Invalid GitHub URL: ${input}`) | ||
} | ||
return fellows | ||
} | ||
/** Compare to another fellow for sorting. */ | ||
compare(other: Fellow): -1 | 0 | 1 { | ||
const A = this.name.toLowerCase() | ||
const B = other.name.toLowerCase() | ||
if (A === B) { | ||
return 0 | ||
} else if (A < B) { | ||
return -1 | ||
/** Get the GitLab URL from the {@link Fellow.gitlabUsername} */ | ||
get gitlabUrl() { | ||
return this.gitlabUsername | ||
? `https://gitlab.com/${this.gitlabUsername}` | ||
: '' | ||
} | ||
/** Set the GitLab URL and username from an input */ | ||
set gitlabUrl(input: string) { | ||
const username = | ||
getUsernameFromGitLabUrl(input) || | ||
getGitLabUsernameFromThanksDevUrl(input) | ||
if (username) { | ||
this.gitlabUsername = username | ||
} else { | ||
return 1 | ||
throw new Error(`Invalid GitLab URL: ${input}`) | ||
} | ||
} | ||
/** | ||
* Compare to another fellow for equivalancy. | ||
* Uses {@link Fellow::idFields} for the comparison. | ||
* @param other The other fellow to compare ourselves with | ||
* @returns Returns `true` if they appear to be the same person, or `false` if not. | ||
*/ | ||
same(other: Fellow): boolean { | ||
for (const field of this.idFields) { | ||
const value = this[field] | ||
const otherValue = other[field] | ||
/** Get the Facebook URL from the {@link Fellow.twitterUsername} */ | ||
get twitterUrl() { | ||
return this.twitterUsername | ||
? `https://twitter.com/${this.twitterUsername}` | ||
: '' | ||
} | ||
/** Set the Twitter URL and username from an input */ | ||
set twitterUrl(input: string) { | ||
const username = getUsernameFromTwitterUrl(input) | ||
if (username) { | ||
this.twitterUsername = username | ||
} else { | ||
throw new Error(`Invalid Twitter URL: ${input}`) | ||
} | ||
} | ||
if (value && otherValue) { | ||
if (value instanceof Set && otherValue instanceof Set) { | ||
for (const item of value) { | ||
if (otherValue.has(item)) { | ||
return true | ||
} | ||
} | ||
} else if (Array.isArray(value) && Array.isArray(otherValue)) { | ||
for (const item of value) { | ||
if (otherValue.includes(item)) { | ||
return true | ||
} | ||
} | ||
} else if (value === otherValue) { | ||
return true | ||
} | ||
/** Get the Facebook URL from the {@link Fellow.facebookUsername} */ | ||
get facebookUrl() { | ||
return this.facebookUsername | ||
? `https://facebook.com/${this.facebookUsername}` | ||
: '' | ||
} | ||
/** Set the Facebook URL and username from an input */ | ||
set facebookUrl(input: string) { | ||
const username = getUsernameFromFacebookUrl(input) | ||
if (username) { | ||
this.facebookUsername = username | ||
} else { | ||
throw new Error(`Invalid Facebook URL: ${input}`) | ||
} | ||
} | ||
/** Get the Patreon URL from the {@link Fellow.patreonUsername} */ | ||
get patreonUrl() { | ||
return this.patreonUsername | ||
? `https://patreon.com/${this.patreonUsername}` | ||
: '' | ||
} | ||
/** Set the Patreon URL and username from an input */ | ||
set patreonUrl(input: string) { | ||
const username = getUsernameFromPatreonUrl(input) | ||
if (username) { | ||
this.patreonUsername = username | ||
} else { | ||
throw new Error(`Invalid Patreon URL: ${input}`) | ||
} | ||
} | ||
/** Get the OpenCollective URL from the {@link Fellow.opencollectiveUsername} */ | ||
get opencollectiveUrl() { | ||
return this.opencollectiveUsername | ||
? `https://opencollective.com/${this.opencollectiveUsername}` | ||
: '' | ||
} | ||
/** Set the OpenCollective URL and username from an input */ | ||
set opencollectiveUrl(input: string) { | ||
const username = getUsernameFromOpenCollectiveUrl(input) | ||
if (username) { | ||
this.opencollectiveUsername = username | ||
} else { | ||
throw new Error(`Invalid OpenCollective URL: ${input}`) | ||
} | ||
} | ||
/** Get the ThanksDev URL from the {@link Fellow.githubUsername} or {@link Fellow.gitlabUsername} */ | ||
get thanksdevUrl() { | ||
return this.githubUsername | ||
? `https://thanks.dev/d/gh/${this.githubUsername}` | ||
: this.gitlabUsername | ||
? `https://thanks.dev/d/gl/${this.gitlabUsername}` | ||
: '' | ||
} | ||
/** Set the ThanksDev URL and username from an input */ | ||
set thanksdevUrl(input: string) { | ||
const githubUsername = getGitHubUsernameFromThanksDevUrl(input) | ||
if (githubUsername) { | ||
this.githubUsername = githubUsername | ||
} else { | ||
const gitlabUsername = getGitLabUsernameFromThanksDevUrl(input) | ||
if (gitlabUsername) { | ||
this.gitlabUsername = gitlabUsername | ||
} else { | ||
throw new Error(`Invalid ThanksDev URL: ${input}`) | ||
} | ||
} | ||
return false | ||
} | ||
/** | ||
* With the value, see if an existing fellow exists in our singleton list property with the value, otherwise create a new fellow instance with the value and add them to our singleton list. | ||
* Uses {@link Fellow::same} for the comparison. | ||
* @param value The value to create a new fellow instance or find the existing fellow instance with | ||
* @param add Whether to add the created person to the list | ||
* @returns The new or existing fellow instance | ||
*/ | ||
static ensure(value: any, add: boolean = true): Fellow { | ||
const newFellow = this.create(value) | ||
for (const existingFellow of this.fellows) { | ||
if (newFellow.same(existingFellow)) { | ||
return existingFellow.set(value) | ||
/** URL fields used to resolve {@link Fellow.url} */ | ||
protected readonly urlFields = [ | ||
'websiteUrl', | ||
'githubUrl', | ||
'gitlabUrl', | ||
'twitterUrl', | ||
'facebookUrl', | ||
'patreonUrl', | ||
'opencollectiveUrl', | ||
'thanksdevUrl', | ||
] | ||
/** Get all unique resolved URLs */ | ||
get urls() { | ||
return this.getFields(this.urlFields) | ||
} | ||
/** Get the first resolved {@link Fellow.urlFields}. Used by GitHub GraphQL API. */ | ||
get url() { | ||
return this.getFirstField(this.urlFields) || '' | ||
} | ||
/** Set the appropriate {@link Fellow.urlFields} from the input */ | ||
set url(input: string) { | ||
input = trim(input) | ||
if (input) { | ||
// convert to https | ||
input = input.replace(/^http:\/\//, 'https://') | ||
// slice 1 to skip websiteUrl | ||
for (const field of this.urlFields) { | ||
// skip websiteUrl in any order, as that is our fallback | ||
if (field === 'websiteUrl') continue | ||
// attempt application of the field | ||
try { | ||
this[field] = input | ||
// the application was successful, it is not a websiteUrl | ||
return | ||
} catch (err: any) { | ||
// the application failed, try the next field | ||
continue | ||
} | ||
} | ||
// all non-websiteUrl applications failed, it must be a websiteUrl | ||
this._websiteUrl = input | ||
} | ||
if (add) { | ||
this.fellows.push(newFellow) | ||
return newFellow | ||
} else { | ||
throw new Error(`Fellow by ${value} does not exist`) | ||
} | ||
// ----------------------------------- | ||
// Emails | ||
/** Emails used */ | ||
readonly emails = new Set<string>() | ||
/** Fetch the first email that was applied, otherwise an empty string */ | ||
get email() { | ||
for (const email of this.emails) { | ||
return email | ||
} | ||
return '' | ||
} | ||
/** | ||
* Get a fellow from the singleton list | ||
* @param value The value to fetch the value with | ||
* @returns The fetched fellow, if they exist with that value | ||
*/ | ||
static get(value: any): Fellow { | ||
return this.ensure(value, false) | ||
/** Add the email to the set instead of replacing it */ | ||
set email(input) { | ||
input = trim(input) | ||
if (input) { | ||
this.emails.add(input) | ||
} | ||
} | ||
// ----------------------------------- | ||
// Description | ||
/** Storage of the description */ | ||
_description: string = '' | ||
/** Get the resolved description */ | ||
get description() { | ||
return this._description | ||
} | ||
/** Set the resolved description */ | ||
set description(input: string) { | ||
input = trim(input) | ||
this._description = input | ||
} | ||
/** Alias for {@link Fellow.description} */ | ||
get bio() { | ||
return this.description | ||
} | ||
/** Alias for {@link Fellow.description} */ | ||
set bio(input: string) { | ||
this.description = input | ||
} | ||
// ----------------------------------- | ||
// Identification | ||
/** | ||
* Add a fellow or a series of people, denoted by the value, to the singleton list | ||
* @param values The fellow or people to add | ||
* @returns An array of the fellow objects for the passed people | ||
* An array of field names that are used to determine if two fellow's are the same. | ||
* Don't need to add usernames for github, twitter, and facebook, as they will be compared via `urls`. | ||
* Can't compare just username, as that is not unique unless comparison's are on the same service, hence why `urls` are used and not `usernames`. | ||
*/ | ||
static add(...values: any[]): Array<Fellow> { | ||
const result: Array<Fellow> = [] | ||
for (const value of values) { | ||
if (value instanceof this) { | ||
result.push(this.ensure(value)) | ||
} else if (typeof value === 'string') { | ||
result.push(...value.split(/, +/).map((fellow) => this.ensure(fellow))) | ||
} else if (Array.isArray(value)) { | ||
result.push(...value.map((value) => this.ensure(value))) | ||
} else if (value) { | ||
result.push(this.ensure(value)) | ||
protected readonly idFields = ['urls', 'emails'] | ||
/** An array of identifiers, all lowercased to prevent typestrong/TypeStrong double-ups */ | ||
get ids() { | ||
const results = new Set<string>() | ||
for (const field of this.idFields) { | ||
const value = this[field] | ||
if (value instanceof Set || Array.isArray(value)) { | ||
for (const item of value) { | ||
results.add(item.toLowerCase()) | ||
} | ||
} else { | ||
return String(value).toLowerCase() | ||
} | ||
} | ||
return result | ||
return Array.from(results.values()).filter(Boolean) | ||
} | ||
/** Create a new Fellow instance with the value, however if the value is already a fellow instance, then just return it */ | ||
static create(value: any) { | ||
return value instanceof this ? value : new this(value) | ||
} | ||
// ----------------------------------- | ||
// Methods | ||
/** | ||
* Construct our fellow instance with the value | ||
* @param value The value used to set the properties of the fellow, forwarded to {@link Fellow::set} | ||
* @param input The value used to set the properties of the fellow, forwarded to {@link Fellow.set} | ||
*/ | ||
constructor(value: any) { | ||
this.set(value) | ||
constructor(input: any) { | ||
this.set(input) | ||
} | ||
@@ -226,5 +510,5 @@ | ||
} | ||
const name = (match[1] || '').trim() | ||
const email = (match[2] || '').trim() | ||
const url = (match[3] || '').trim() | ||
const name = trim(match[1]) | ||
const email = trim(match[2]) | ||
const url = trim(match[3]) | ||
if (name) this.name = name | ||
@@ -239,16 +523,7 @@ if (email) this.email = email | ||
if (key[0] === '_') return // skip if private | ||
const value = fellow[key] || null | ||
if (value) { | ||
// if any of the url fields, redirect to url setter | ||
if (this.urlFields.includes(key)) { | ||
this.url = value | ||
} | ||
// if not a url field, e.g. name or email | ||
else { | ||
this[key] = value | ||
} | ||
} | ||
const value = trim(fellow[key]) | ||
if (value) this[key] = value | ||
}) | ||
} else { | ||
throw new Error('Invalid fellow input') | ||
throw new Error(`Invalid Fellow input: ${JSON.stringify(fellow)}`) | ||
} | ||
@@ -259,140 +534,224 @@ | ||
// ----------------------------------- | ||
// Accessors | ||
/** Compare to another fellow for sorting. */ | ||
compare(other: Fellow): -1 | 0 | 1 { | ||
const a = this.name.toLowerCase() | ||
const b = other.name.toLowerCase() | ||
if (a === b) { | ||
return 0 | ||
} else if (a < b) { | ||
return -1 | ||
} else { | ||
return 1 | ||
} | ||
} | ||
/** | ||
* If the name is empty, we will try to fallback to githubUsername then twitterUsername | ||
* If the name is prefixed with a series of numbers, that is considered the year | ||
* E.g. In `2015+ Bevry Pty Ltd` then `2015+` is the years | ||
* E.g. In `2013-2015 Bevry Pty Ltd` then `2013-2015` is the years | ||
* Compare to another fellow for equivalency. | ||
* Uses {@link Fellow.ids} for the comparison. | ||
* @param other The other fellow to compare ourselves with | ||
* @returns Returns `true` if they appear to be the same person, or `false` if not. | ||
*/ | ||
set name(value /* :string */) { | ||
const match = /^((?:[0-9]+[-+]?)+)?(.+)$/.exec(value) | ||
if (match) { | ||
// fetch the years, but for now, discard it | ||
const years = String(match[1] || '').trim() | ||
if (years) this.years = years | ||
// fetch the name, and apply it | ||
const name = match[2].trim() | ||
if (name) this._name = name | ||
same(other: Fellow): boolean { | ||
const ids = new Set(this.ids) | ||
const otherIds = new Set(other.ids) | ||
for (const id of ids) { | ||
if (otherIds.has(id)) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
// ----------------------------------- | ||
// Static | ||
/** | ||
* Fetch the user's name, otherwise their usernames | ||
* Sort a list of fellows. | ||
* Uses {@link Fellow.compare} for the comparison. | ||
*/ | ||
get name(): string { | ||
return ( | ||
this._name || | ||
this.githubUsername || | ||
this.twitterUsername || | ||
this.facebookUsername || | ||
'' | ||
) | ||
static sort(list: Array<Fellow> | Set<Fellow>) { | ||
if (list instanceof Set) { | ||
list = Array.from(list.values()) | ||
} | ||
list = list.sort(comparator) | ||
return list | ||
} | ||
/** Add the email to the set instead of replacing it */ | ||
set email(value) { | ||
if (value) { | ||
this.emails.add(value) | ||
/** Flatten lists of fellows into one set of fellows */ | ||
static flatten(lists: Array<Array<Fellow> | Set<Fellow>>): Set<Fellow> { | ||
const fellows = new Set<Fellow>() | ||
for (const list of lists) { | ||
for (const fellow of list) { | ||
fellows.add(fellow) | ||
} | ||
} | ||
return fellows | ||
} | ||
/** Fetch the first email that was applied, otherwise an empty string */ | ||
get email() { | ||
for (const email of this.emails) { | ||
return email | ||
/** | ||
* With the value, see if an existing fellow exists in our singleton list property with the value, otherwise create a new fellow instance with the value and add them to our singleton list. | ||
* Uses {@link Fellow.same} for the comparison. | ||
* @param input The value to create a new fellow instance or find the existing fellow instance with | ||
* @param add Whether to add the created person to the list | ||
* @returns The new or existing fellow instance | ||
*/ | ||
static ensure(input: any, add: boolean = true): Fellow { | ||
if (input instanceof Fellow && this.fellows.includes(input)) return input | ||
const newFellow = this.create(input) | ||
for (const existingFellow of this.fellows) { | ||
if (newFellow.same(existingFellow)) { | ||
return existingFellow.set(input) | ||
} | ||
} | ||
return '' | ||
if (add) { | ||
this.fellows.push(newFellow) | ||
return newFellow | ||
} else { | ||
throw new Error(`Fellow does not exist: ${input}`) | ||
} | ||
} | ||
/** | ||
* Will determine if the passed URL is a github, facebook, or twitter URL. | ||
* If it is, then it will extract the username and url from it. | ||
* If it was not, then it will set the homepage variable. | ||
* Get a fellow from the singleton list | ||
* @param input The value to fetch the value with | ||
* @returns The fetched fellow, if they exist with that value | ||
*/ | ||
set url(input: string) { | ||
if (input) { | ||
let url: string | ||
// github | ||
const githubMatch = /^.+github.com\/([^/]+)\/?$/.exec(input) | ||
if (githubMatch) { | ||
this.githubUsername = githubMatch[1] | ||
url = this.githubUrl = 'https://github.com/' + this.githubUsername | ||
} else { | ||
const facebookMatch = /^.+facebook.com\/([^/]+)\/?$/.exec(input) | ||
if (facebookMatch) { | ||
this.facebookUsername = facebookMatch[1] | ||
url = this.facebookUrl = | ||
'https://facebook.com/' + this.facebookUsername | ||
} else { | ||
const twitterMatch = /^.+twitter.com\/([^/]+)\/?$/.exec(input) | ||
if (twitterMatch) { | ||
this.twitterUsername = twitterMatch[1] | ||
url = this.twitterUrl = | ||
'https://twitter.com/' + this.twitterUsername | ||
} else { | ||
url = this.homepage = input | ||
} | ||
static get(input: any): Fellow { | ||
return this.ensure(input, false) | ||
} | ||
/** | ||
* Add a fellow or a series of people, denoted by the value, to the singleton list | ||
* @param inputs The fellow or people to add | ||
* @returns A de-duplicated array of fellow objects for the passed people | ||
*/ | ||
static add(...inputs: any[]): Array<Fellow> { | ||
const list = new Set<Fellow>() | ||
for (const input of inputs) { | ||
if (input instanceof this) { | ||
list.add(this.ensure(input)) | ||
} else if (typeof input === 'string') { | ||
for (const item of input.split(/, +/)) { | ||
list.add(this.ensure(item)) | ||
} | ||
} else if (input instanceof Set || Array.isArray(input)) { | ||
for (const item of input) { | ||
list.add(this.ensure(item)) | ||
} | ||
} else if (input) { | ||
list.add(this.ensure(input)) | ||
} | ||
// add url in encrypted and unecrypted forms to urls | ||
this.urls.add(url.replace(/^http:/, 'https:')) | ||
this.urls.add(url.replace(/^https:/, 'http:')) | ||
} | ||
return Array.from(list.values()) | ||
} | ||
/** Fetch the homepage with fallback to one of the service URLs if available */ | ||
get url() { | ||
return ( | ||
this.homepage || | ||
this.githubUrl || | ||
this.facebookUrl || | ||
this.twitterUrl || | ||
'' | ||
/** Create a new Fellow instance with the value, however if the value is already a fellow instance, then just return it */ | ||
static create(value: any) { | ||
return value instanceof this ? value : new this(value) | ||
} | ||
// ----------------------------------- | ||
// Repositories | ||
/** Set of GitHub repository slugs that the fellow authors */ | ||
readonly authorOfRepositories = new Set<string>() | ||
/** Get all fellows who author a particular GitHub repository */ | ||
static authorsOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.authorOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
/** Get the field field from the list that isn't empty */ | ||
getFirstField(fields: string[]) { | ||
for (const field of fields) { | ||
const value = this[field] | ||
if (value) return value | ||
} | ||
return null | ||
/** Set of GitHub repository slugs that the fellow maintains */ | ||
readonly maintainerOfRepositories = new Set<string>() | ||
/** Get all fellows who maintain a particular GitHub repository */ | ||
static maintainersOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.maintainerOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
// ----------------------------------- | ||
// Repositories | ||
/** Map of GitHub repository slugs to the contribution count of the user */ | ||
readonly contributionsOfRepository = new Map<string, number>() | ||
/** Get all fellows who administrate a particular repository */ | ||
static administersRepository(repoSlug: string): Array<Fellow> { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.administeredRepositories.has(repoSlug) | ||
}) | ||
/** Set of GitHub repository slugs that the fellow contributes to */ | ||
readonly contributorOfRepositories = new Set<string>() | ||
/** Get all fellows who contribute to a particular GitHub repository */ | ||
static contributorsOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.contributorOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
/** Get all fellows who contribute to a particular repository */ | ||
static contributesRepository(repoSlug: string): Array<Fellow> { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.contributedRepositories.has(repoSlug) | ||
}) | ||
/** Set of GitHub repository slugs that the fellow initially financed */ | ||
readonly funderOfRepositories = new Set<string>() | ||
/** Get all fellows who initally financed a particular GitHub repository */ | ||
static fundersOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.funderOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
/** Get all fellows who maintain a particular repository */ | ||
static maintainsRepository(repoSlug: string): Array<Fellow> { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.maintainedRepositories.has(repoSlug) | ||
}) | ||
/** Set of GitHub repository slugs that the fellow actively finances */ | ||
readonly sponsorOfRepositories = new Set<string>() | ||
/** Get all fellows who actively finance a particular GitHub repository */ | ||
static sponsorsOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.sponsorOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
/** Get all fellows who author a particular repository */ | ||
static authorsRepository(repoSlug: string): Array<Fellow> { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.authoredRepositories.has(repoSlug) | ||
}) | ||
/** Set of GitHub repository slugs that the fellow has historically financed */ | ||
readonly donorOfRepositories = new Set<string>() | ||
/** Get all fellows who have historically financed a particular GitHub repository */ | ||
static donorsOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.donorOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
// @todo figure out how to calculate this | ||
// /** Map of GitHub repository slugs to the sponsorship amount of the user */ | ||
// readonly sponsorships = new Map<string, number>() | ||
// ----------------------------------- | ||
// Formats | ||
// Formatting | ||
/** Get the first field from the list that isn't empty */ | ||
getFirstField(fields: string[]) { | ||
for (const field of fields) { | ||
const value = this[field] | ||
if (value) return value | ||
} | ||
return null | ||
} | ||
/** Get the all the de-duplicated fields from the list that aren't empty */ | ||
getFields(fields: string[]) { | ||
const set = new Set<string>() | ||
for (const field of fields) { | ||
const value = this[field] | ||
if (value) set.add(value) | ||
} | ||
return Array.from(set.values()) | ||
} | ||
/** | ||
@@ -414,3 +773,3 @@ * Convert the fellow into the usual string format | ||
if (format.displayEmail && this.email) { | ||
if (format.displayEmail !== false && this.email) { | ||
parts.push(`<${this.email}>`) | ||
@@ -432,2 +791,35 @@ } | ||
/** | ||
* Convert the fellow into the usual text format | ||
* @example `NAME 📝 DESCRIPTION 🔗 URL` | ||
*/ | ||
toText(format: FormatOptions = {}): string { | ||
if (!this.name) return '' | ||
const parts = [] | ||
// prefix | ||
if (format.prefix) parts.push(format.prefix) | ||
// name | ||
parts.push(`${this.name}`) | ||
if (format.displayEmail && this.email) { | ||
parts.push(`✉️ ${this.email}`) | ||
} | ||
// description | ||
if (format.displayDescription !== false && this.description) { | ||
parts.push(`📝 ${this.description}`) | ||
} | ||
// url | ||
if (format.displayUrl !== false && this.url) { | ||
parts.push(`🔗 ${this.url}`) | ||
} | ||
// return | ||
return parts.join(' ') | ||
} | ||
/** | ||
* Convert the fellow into the usual markdown format | ||
@@ -462,5 +854,5 @@ * @example `[NAME](URL) <EMAIL>` | ||
) { | ||
const contributionsURL = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}` | ||
const contributionsUrl = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}` | ||
parts.push( | ||
`— [view contributions](${contributionsURL} "View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}")`, | ||
`— [view contributions](${contributionsUrl} "View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}")` | ||
) | ||
@@ -494,3 +886,3 @@ } | ||
parts.push( | ||
`<a href="mailto:${this.email}" title="Email ${this.name}"><${this.email}></a>`, | ||
`<a href="mailto:${this.email}" title="Email ${this.name}"><${this.email}></a>` | ||
) | ||
@@ -505,5 +897,5 @@ } | ||
) { | ||
const contributionsURL = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}` | ||
const contributionsUrl = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}` | ||
parts.push( | ||
`— <a href="${contributionsURL}" title="View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}">view contributions</a>`, | ||
`— <a href="${contributionsUrl}" title="View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}">view contributions</a>` | ||
) | ||
@@ -510,0 +902,0 @@ } |
169
package.json
{ | ||
"name": "fellow", | ||
"version": "6.25.0", | ||
"version": "7.0.0-next.1703075602.d3e1a4be5066f75a2b484559e0c4f5b5a631a61b", | ||
"description": "Fellow is a package for creating people that can be unified by their shared values via a singleton list on the class", | ||
@@ -13,3 +13,9 @@ "homepage": "https://github.com/bevry/fellow", | ||
"contributors", | ||
"es2017", | ||
"deno", | ||
"deno-edition", | ||
"deno-entry", | ||
"denoland", | ||
"es2016", | ||
"es2022", | ||
"es5", | ||
"export-default", | ||
@@ -38,3 +44,2 @@ "fellow", | ||
"patreon", | ||
"flattr", | ||
"liberapay", | ||
@@ -45,3 +50,5 @@ "buymeacoffee", | ||
"paypal", | ||
"wishlist" | ||
"---", | ||
"discord", | ||
"twitch" | ||
], | ||
@@ -51,3 +58,3 @@ "config": { | ||
"githubSponsorsUsername": "balupton", | ||
"thanksdevGithubUsername": "balupton", | ||
"thanksdevGithubUsername": "bevry", | ||
"buymeacoffeeUsername": "balupton", | ||
@@ -61,2 +68,5 @@ "cryptoURL": "https://bevry.me/crypto", | ||
"wishlistURL": "https://bevry.me/wishlist", | ||
"discordServerID": "1147436445783560193", | ||
"discordServerInvite": "nQuXddV7VP", | ||
"twitchUsername": "balupton", | ||
"githubUsername": "bevry", | ||
@@ -113,4 +123,4 @@ "githubRepository": "fellow", | ||
{ | ||
"description": "TypeScript compiled against ES2017 for Node.js with Require for modules", | ||
"directory": "edition-es2017", | ||
"description": "TypeScript compiled against ES2022 for Node.js 12 || 14 || 16 || 18 || 20 || 21 with Require for modules", | ||
"directory": "edition-es2022", | ||
"entry": "index.js", | ||
@@ -120,7 +130,7 @@ "tags": [ | ||
"javascript", | ||
"es2017", | ||
"es2022", | ||
"require" | ||
], | ||
"engines": { | ||
"node": "10 || 12 || 14 || 16 || 18 || 20 || 21", | ||
"node": "12 || 14 || 16 || 18 || 20 || 21", | ||
"browsers": false | ||
@@ -130,4 +140,4 @@ } | ||
{ | ||
"description": "TypeScript compiled against ES2017 for Node.js with Import for modules", | ||
"directory": "edition-es2017-esm", | ||
"description": "TypeScript compiled against ES2016 for Node.js 6 || 8 || 10 || 12 || 14 || 16 || 18 || 20 || 21 with Require for modules", | ||
"directory": "edition-es2016", | ||
"entry": "index.js", | ||
@@ -137,3 +147,33 @@ "tags": [ | ||
"javascript", | ||
"es2017", | ||
"es2016", | ||
"require" | ||
], | ||
"engines": { | ||
"node": "6 || 8 || 10 || 12 || 14 || 16 || 18 || 20 || 21", | ||
"browsers": false | ||
} | ||
}, | ||
{ | ||
"description": "TypeScript compiled against ES5 for Node.js 4 || 6 || 8 || 10 || 12 || 14 || 16 || 18 || 20 || 21 with Require for modules", | ||
"directory": "edition-es5", | ||
"entry": "index.js", | ||
"tags": [ | ||
"compiled", | ||
"javascript", | ||
"es5", | ||
"require" | ||
], | ||
"engines": { | ||
"node": "4 || 6 || 8 || 10 || 12 || 14 || 16 || 18 || 20 || 21", | ||
"browsers": false | ||
} | ||
}, | ||
{ | ||
"description": "TypeScript compiled against ES2022 for Node.js 12 || 14 || 16 || 18 || 20 || 21 with Import for modules", | ||
"directory": "edition-es2022-esm", | ||
"entry": "index.js", | ||
"tags": [ | ||
"compiled", | ||
"javascript", | ||
"es2022", | ||
"import" | ||
@@ -145,52 +185,84 @@ ], | ||
} | ||
}, | ||
{ | ||
"description": "TypeScript compiled Types with Import for modules", | ||
"directory": "edition-types", | ||
"entry": "index.d.ts", | ||
"tags": [ | ||
"compiled", | ||
"types", | ||
"import" | ||
], | ||
"engines": false | ||
}, | ||
{ | ||
"description": "TypeScript source code made to be compatible with Deno", | ||
"directory": "edition-deno", | ||
"entry": "index.ts", | ||
"tags": [ | ||
"typescript", | ||
"import", | ||
"deno" | ||
], | ||
"engines": { | ||
"deno": true, | ||
"browsers": true | ||
} | ||
} | ||
], | ||
"types": "./compiled-types/", | ||
"types": "edition-types/index.d.ts", | ||
"type": "module", | ||
"main": "edition-es2017/index.js", | ||
"main": "index.cjs", | ||
"exports": { | ||
"node": { | ||
"import": "./edition-es2017-esm/index.js", | ||
"require": "./edition-es2017/index.js" | ||
"types": "./edition-types/index.d.ts", | ||
"import": "./edition-es2022-esm/index.js", | ||
"default": "./index.cjs", | ||
"require": "./edition-es2022/index.js" | ||
}, | ||
"browser": { | ||
"types": "./edition-types/index.d.ts", | ||
"import": "./edition-browsers/index.js" | ||
} | ||
}, | ||
"deno": "edition-deno/index.ts", | ||
"browser": "edition-browsers/index.js", | ||
"module": "edition-browsers/index.js", | ||
"dependencies": { | ||
"editions": "^6.19.0" | ||
}, | ||
"devDependencies": { | ||
"@bevry/update-contributors": "^1.22.0", | ||
"@types/node": "^20.8.10", | ||
"@typescript-eslint/eslint-plugin": "^6.9.1", | ||
"@typescript-eslint/parser": "^6.9.1", | ||
"assert-helpers": "^8.4.0", | ||
"eslint": "^8.52.0", | ||
"eslint-config-bevry": "^3.27.0", | ||
"eslint-config-prettier": "^9.0.0", | ||
"@bevry/update-contributors": "^1.23.0", | ||
"@types/node": "^20.10.5", | ||
"@typescript-eslint/eslint-plugin": "^6.15.0", | ||
"@typescript-eslint/parser": "^6.15.0", | ||
"assert-helpers": "^11.9.0", | ||
"eslint": "^8.56.0", | ||
"eslint-config-bevry": "^5.3.0", | ||
"eslint-config-prettier": "^9.1.0", | ||
"eslint-plugin-babel": "^5.3.1", | ||
"eslint-plugin-prettier": "^5.0.1", | ||
"kava": "^5.15.0", | ||
"make-deno-edition": "^1.3.0", | ||
"prettier": "^3.0.3", | ||
"projectz": "^2.23.0", | ||
"surge": "^0.23.1", | ||
"typedoc": "^0.25.3", | ||
"typescript": "5.2.2", | ||
"valid-directory": "^4.0.0", | ||
"valid-module": "^1.17.0" | ||
"eslint-plugin-prettier": "^5.1.0", | ||
"kava": "^7.5.0", | ||
"make-deno-edition": "^2.1.0", | ||
"prettier": "^3.1.1", | ||
"projectz": "^3.4.0", | ||
"typedoc": "^0.25.4", | ||
"typescript": "5.3.3", | ||
"valid-directory": "^4.7.0", | ||
"valid-module": "^2.6.0" | ||
}, | ||
"scripts": { | ||
"our:clean": "rm -Rf ./docs ./edition* ./es2015 ./es5 ./out ./.next", | ||
"our:compile": "npm run our:compile:deno && npm run our:compile:edition-browsers && npm run our:compile:edition-es2017 && npm run our:compile:edition-es2017-esm && npm run our:compile:types", | ||
"our:clean": "rm -rf ./docs ./edition* ./es2015 ./es5 ./out ./.next", | ||
"our:compile": "npm run our:compile:deno && npm run our:compile:edition-browsers && npm run our:compile:edition-es2016 && npm run our:compile:edition-es2022 && npm run our:compile:edition-es2022-esm && npm run our:compile:edition-es5 && npm run our:compile:edition-types", | ||
"our:compile:deno": "make-deno-edition --attempt", | ||
"our:compile:edition-browsers": "tsc --module ESNext --target ES2022 --outDir ./edition-browsers --project tsconfig.json && ( test ! -d edition-browsers/source || ( mv edition-browsers/source edition-temp && rm -Rf edition-browsers && mv edition-temp edition-browsers ) )", | ||
"our:compile:edition-es2017": "tsc --module commonjs --target ES2017 --outDir ./edition-es2017 --project tsconfig.json && ( test ! -d edition-es2017/source || ( mv edition-es2017/source edition-temp && rm -Rf edition-es2017 && mv edition-temp edition-es2017 ) ) && printf '%s' '{\"type\": \"commonjs\"}' > edition-es2017/package.json", | ||
"our:compile:edition-es2017-esm": "tsc --module ESNext --target ES2017 --outDir ./edition-es2017-esm --project tsconfig.json && ( test ! -d edition-es2017-esm/source || ( mv edition-es2017-esm/source edition-temp && rm -Rf edition-es2017-esm && mv edition-temp edition-es2017-esm ) ) && printf '%s' '{\"type\": \"module\"}' > edition-es2017-esm/package.json", | ||
"our:compile:types": "tsc --project tsconfig.json --emitDeclarationOnly --declaration --declarationMap --declarationDir ./compiled-types && ( test ! -d compiled-types/source || ( mv compiled-types/source edition-temp && rm -Rf compiled-types && mv edition-temp compiled-types ) )", | ||
"our:compile:edition-browsers": "tsc --module ESNext --target ES2022 --outDir ./edition-browsers --project tsconfig.json && ( test ! -d edition-browsers/source || ( mv edition-browsers/source edition-temp && rm -rf edition-browsers && mv edition-temp edition-browsers ) )", | ||
"our:compile:edition-es2016": "tsc --module commonjs --target ES2016 --outDir ./edition-es2016 --project tsconfig.json && ( test ! -d edition-es2016/source || ( mv edition-es2016/source edition-temp && rm -rf edition-es2016 && mv edition-temp edition-es2016 ) ) && printf '%s' '{\"type\": \"commonjs\"}' > edition-es2016/package.json", | ||
"our:compile:edition-es2022": "tsc --module commonjs --target ES2022 --outDir ./edition-es2022 --project tsconfig.json && ( test ! -d edition-es2022/source || ( mv edition-es2022/source edition-temp && rm -rf edition-es2022 && mv edition-temp edition-es2022 ) ) && printf '%s' '{\"type\": \"commonjs\"}' > edition-es2022/package.json", | ||
"our:compile:edition-es2022-esm": "tsc --module ESNext --target ES2022 --outDir ./edition-es2022-esm --project tsconfig.json && ( test ! -d edition-es2022-esm/source || ( mv edition-es2022-esm/source edition-temp && rm -rf edition-es2022-esm && mv edition-temp edition-es2022-esm ) ) && printf '%s' '{\"type\": \"module\"}' > edition-es2022-esm/package.json", | ||
"our:compile:edition-es5": "tsc --module commonjs --target ES5 --outDir ./edition-es5 --project tsconfig.json && ( test ! -d edition-es5/source || ( mv edition-es5/source edition-temp && rm -rf edition-es5 && mv edition-temp edition-es5 ) ) && printf '%s' '{\"type\": \"commonjs\"}' > edition-es5/package.json", | ||
"our:compile:edition-types": "tsc --emitDeclarationOnly --declaration --declarationMap --declarationDir ./edition-types --project tsconfig.json && ( test ! -d edition-types/source || ( mv edition-types/source edition-temp && rm -rf edition-types && mv edition-temp edition-types ) )", | ||
"our:deploy": "printf '%s\n' 'no need for this project'", | ||
"our:meta": "npm run our:meta:contributors && npm run our:meta:docs && npm run our:meta:projectz", | ||
"our:meta:contributors": "update-contributors", | ||
"our:meta": "npm run our:meta:docs && npm run our:meta:projectz", | ||
"our:meta:docs": "npm run our:meta:docs:typedoc", | ||
"our:meta:docs:typedoc": "rm -Rf ./docs && typedoc --exclude '**/+(*test*|node_modules)' --excludeExternals --out ./docs ./source", | ||
"our:meta:docs:typedoc": "rm -rf ./docs && typedoc --exclude '**/+(*test*|node_modules)' --excludeExternals --out ./docs ./source", | ||
"our:meta:projectz": "projectz compile", | ||
@@ -206,8 +278,7 @@ "our:release": "npm run our:release:prepare && npm run our:release:check-changelog && npm run our:release:check-dirty && npm run our:release:tag && npm run our:release:push", | ||
"our:test": "npm run our:verify && npm test", | ||
"our:verify": "npm run our:verify:directory && npm run our:verify:eslint && npm run our:verify:module && npm run our:verify:prettier", | ||
"our:verify:directory": "valid-directory", | ||
"our:verify": "npm run our:verify:eslint && npm run our:verify:module && npm run our:verify:prettier", | ||
"our:verify:eslint": "eslint --fix --ignore-pattern '**/*.d.ts' --ignore-pattern '**/vendor/' --ignore-pattern '**/node_modules/' --ext .mjs,.js,.jsx,.ts,.tsx ./source", | ||
"our:verify:module": "valid-module", | ||
"our:verify:prettier": "prettier --write .", | ||
"test": "node ./edition-es2017/test.js" | ||
"test": "node ./test.cjs" | ||
}, | ||
@@ -221,4 +292,6 @@ "eslintConfig": { | ||
"semi": false, | ||
"singleQuote": true | ||
"singleQuote": true, | ||
"trailingComma": "es5", | ||
"endOfLine": "lf" | ||
} | ||
} | ||
} |
@@ -15,5 +15,4 @@ <!-- TITLE/ --> | ||
<span class="badge-githubsponsors"><a href="https://github.com/sponsors/balupton" title="Donate to this project using GitHub Sponsors"><img src="https://img.shields.io/badge/github-donate-yellow.svg" alt="GitHub Sponsors donate button" /></a></span> | ||
<span class="badge-thanksdev"><a href="https://thanks.dev/u/gh/balupton" title="Donate to this project using ThanksDev"><img src="https://img.shields.io/badge/thanksdev-donate-yellow.svg" alt="ThanksDev donate button" /></a></span> | ||
<span class="badge-thanksdev"><a href="https://thanks.dev/u/gh/bevry" title="Donate to this project using ThanksDev"><img src="https://img.shields.io/badge/thanksdev-donate-yellow.svg" alt="ThanksDev donate button" /></a></span> | ||
<span class="badge-patreon"><a href="https://patreon.com/bevry" title="Donate to this project using Patreon"><img src="https://img.shields.io/badge/patreon-donate-yellow.svg" alt="Patreon donate button" /></a></span> | ||
<span class="badge-flattr"><a href="https://flattr.com/profile/balupton" title="Donate to this project using Flattr"><img src="https://img.shields.io/badge/flattr-donate-yellow.svg" alt="Flattr donate button" /></a></span> | ||
<span class="badge-liberapay"><a href="https://liberapay.com/bevry" title="Donate to this project using Liberapay"><img src="https://img.shields.io/badge/liberapay-donate-yellow.svg" alt="Liberapay donate button" /></a></span> | ||
@@ -24,3 +23,5 @@ <span class="badge-buymeacoffee"><a href="https://buymeacoffee.com/balupton" title="Donate to this project using Buy Me A Coffee"><img src="https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg" alt="Buy Me A Coffee donate button" /></a></span> | ||
<span class="badge-paypal"><a href="https://bevry.me/paypal" title="Donate to this project using Paypal"><img src="https://img.shields.io/badge/paypal-donate-yellow.svg" alt="PayPal donate button" /></a></span> | ||
<span class="badge-wishlist"><a href="https://bevry.me/wishlist" title="Buy an item on our wishlist for us"><img src="https://img.shields.io/badge/wishlist-donate-yellow.svg" alt="Wishlist browse button" /></a></span> | ||
<br class="badge-separator" /> | ||
<span class="badge-discord"><a href="https://discord.gg/nQuXddV7VP" title="Join this project's community on Discord"><img src="https://img.shields.io/discord/1147436445783560193?logo=discord&label=discord" alt="Discord server badge" /></a></span> | ||
<span class="badge-twitch"><a href="https://www.twitch.tv/balupton" title="Join this project's community on Twitch"><img src="https://img.shields.io/twitch/status/balupton?logo=twitch" alt="Twitch community badge" /></a></span> | ||
@@ -52,2 +53,8 @@ <!-- /BADGES --> | ||
<a href="https://deno.land" title="Deno is a secure runtime for JavaScript and TypeScript, it is an alternative for Node.js"><h3>Deno</h3></a> | ||
``` typescript | ||
import pkg from 'https://unpkg.com/fellow@^7.0.0/edition-deno/index.ts' | ||
``` | ||
<a href="https://www.skypack.dev" title="Skypack is a JavaScript Delivery Network for modern web apps"><h3>Skypack</h3></a> | ||
@@ -57,3 +64,3 @@ | ||
<script type="module"> | ||
import pkg from '//cdn.skypack.dev/fellow@^6.25.0' | ||
import pkg from '//cdn.skypack.dev/fellow@^7.0.0' | ||
</script> | ||
@@ -66,3 +73,3 @@ ``` | ||
<script type="module"> | ||
import pkg from '//unpkg.com/fellow@^6.25.0' | ||
import pkg from '//unpkg.com/fellow@^7.0.0' | ||
</script> | ||
@@ -75,3 +82,3 @@ ``` | ||
<script type="module"> | ||
import pkg from '//dev.jspm.io/fellow@6.25.0' | ||
import pkg from '//dev.jspm.io/fellow@7.0.0' | ||
</script> | ||
@@ -84,7 +91,11 @@ ``` | ||
<ul><li><code>fellow/source/index.ts</code> is <a href="https://www.typescriptlang.org/" title="TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. ">TypeScript</a> source code with <a href="https://babeljs.io/docs/learn-es2015/#modules" title="ECMAScript Modules">Import</a> for modules</li> | ||
<ul><li><code>fellow</code> aliases <code>fellow/index.cjs</code> which uses the <a href="https://github.com/bevry/editions" title="You can use the Editions Autoloader to autoload the appropriate edition for your consumers environment">Editions Autoloader</a> to automatically select the correct edition for the consumer's environment</li> | ||
<li><code>fellow/source/index.ts</code> is <a href="https://www.typescriptlang.org/" title="TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. ">TypeScript</a> source code with <a href="https://babeljs.io/docs/learn-es2015/#modules" title="ECMAScript Modules">Import</a> for modules</li> | ||
<li><code>fellow/edition-browsers/index.js</code> is <a href="https://www.typescriptlang.org/" title="TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. ">TypeScript</a> compiled against ES2022 for web browsers with <a href="https://babeljs.io/docs/learn-es2015/#modules" title="ECMAScript Modules">Import</a> for modules</li> | ||
<li><code>fellow</code> aliases <code>fellow/edition-es2017/index.js</code></li> | ||
<li><code>fellow/edition-es2017/index.js</code> is <a href="https://www.typescriptlang.org/" title="TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. ">TypeScript</a> compiled against <a href="https://en.wikipedia.org/wiki/ECMAScript#8th_Edition_-_ECMAScript_2017" title="ECMAScript ES2017">ES2017</a> for <a href="https://nodejs.org" title="Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine">Node.js</a> with <a href="https://nodejs.org/dist/latest-v5.x/docs/api/modules.html" title="Node/CJS Modules">Require</a> for modules</li> | ||
<li><code>fellow/edition-es2017-esm/index.js</code> is <a href="https://www.typescriptlang.org/" title="TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. ">TypeScript</a> compiled against <a href="https://en.wikipedia.org/wiki/ECMAScript#8th_Edition_-_ECMAScript_2017" title="ECMAScript ES2017">ES2017</a> for <a href="https://nodejs.org" title="Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine">Node.js</a> with <a href="https://babeljs.io/docs/learn-es2015/#modules" title="ECMAScript Modules">Import</a> for modules</li></ul> | ||
<li><code>fellow/edition-es2022/index.js</code> is <a href="https://www.typescriptlang.org/" title="TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. ">TypeScript</a> compiled against ES2022 for <a href="https://nodejs.org" title="Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine">Node.js</a> 12 || 14 || 16 || 18 || 20 || 21 with <a href="https://nodejs.org/dist/latest-v5.x/docs/api/modules.html" title="Node/CJS Modules">Require</a> for modules</li> | ||
<li><code>fellow/edition-es2016/index.js</code> is <a href="https://www.typescriptlang.org/" title="TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. ">TypeScript</a> compiled against <a href="https://en.wikipedia.org/wiki/ECMAScript#7th_Edition_-_ECMAScript_2016" title="ECMAScript 2016">ES2016</a> for <a href="https://nodejs.org" title="Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine">Node.js</a> 6 || 8 || 10 || 12 || 14 || 16 || 18 || 20 || 21 with <a href="https://nodejs.org/dist/latest-v5.x/docs/api/modules.html" title="Node/CJS Modules">Require</a> for modules</li> | ||
<li><code>fellow/edition-es5/index.js</code> is <a href="https://www.typescriptlang.org/" title="TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. ">TypeScript</a> compiled against ES5 for <a href="https://nodejs.org" title="Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine">Node.js</a> 4 || 6 || 8 || 10 || 12 || 14 || 16 || 18 || 20 || 21 with <a href="https://nodejs.org/dist/latest-v5.x/docs/api/modules.html" title="Node/CJS Modules">Require</a> for modules</li> | ||
<li><code>fellow/edition-es2022-esm/index.js</code> is <a href="https://www.typescriptlang.org/" title="TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. ">TypeScript</a> compiled against ES2022 for <a href="https://nodejs.org" title="Node.js is a JavaScript runtime built on Chrome's V8 JavaScript engine">Node.js</a> 12 || 14 || 16 || 18 || 20 || 21 with <a href="https://babeljs.io/docs/learn-es2015/#modules" title="ECMAScript Modules">Import</a> for modules</li> | ||
<li><code>fellow/edition-types/index.d.ts</code> is <a href="https://www.typescriptlang.org/" title="TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. ">TypeScript</a> compiled Types with <a href="https://babeljs.io/docs/learn-es2015/#modules" title="ECMAScript Modules">Import</a> for modules</li> | ||
<li><code>fellow/edition-deno/index.ts</code> is <a href="https://www.typescriptlang.org/" title="TypeScript is a typed superset of JavaScript that compiles to plain JavaScript. ">TypeScript</a> source code made to be compatible with <a href="https://deno.land" title="Deno is a secure runtime for JavaScript and TypeScript, it is an alternative to Node.js">Deno</a></li></ul> | ||
@@ -127,5 +138,4 @@ <!-- /INSTALL --> | ||
<span class="badge-githubsponsors"><a href="https://github.com/sponsors/balupton" title="Donate to this project using GitHub Sponsors"><img src="https://img.shields.io/badge/github-donate-yellow.svg" alt="GitHub Sponsors donate button" /></a></span> | ||
<span class="badge-thanksdev"><a href="https://thanks.dev/u/gh/balupton" title="Donate to this project using ThanksDev"><img src="https://img.shields.io/badge/thanksdev-donate-yellow.svg" alt="ThanksDev donate button" /></a></span> | ||
<span class="badge-thanksdev"><a href="https://thanks.dev/u/gh/bevry" title="Donate to this project using ThanksDev"><img src="https://img.shields.io/badge/thanksdev-donate-yellow.svg" alt="ThanksDev donate button" /></a></span> | ||
<span class="badge-patreon"><a href="https://patreon.com/bevry" title="Donate to this project using Patreon"><img src="https://img.shields.io/badge/patreon-donate-yellow.svg" alt="Patreon donate button" /></a></span> | ||
<span class="badge-flattr"><a href="https://flattr.com/profile/balupton" title="Donate to this project using Flattr"><img src="https://img.shields.io/badge/flattr-donate-yellow.svg" alt="Flattr donate button" /></a></span> | ||
<span class="badge-liberapay"><a href="https://liberapay.com/bevry" title="Donate to this project using Liberapay"><img src="https://img.shields.io/badge/liberapay-donate-yellow.svg" alt="Liberapay donate button" /></a></span> | ||
@@ -136,3 +146,2 @@ <span class="badge-buymeacoffee"><a href="https://buymeacoffee.com/balupton" title="Donate to this project using Buy Me A Coffee"><img src="https://img.shields.io/badge/buy%20me%20a%20coffee-donate-yellow.svg" alt="Buy Me A Coffee donate button" /></a></span> | ||
<span class="badge-paypal"><a href="https://bevry.me/paypal" title="Donate to this project using Paypal"><img src="https://img.shields.io/badge/paypal-donate-yellow.svg" alt="PayPal donate button" /></a></span> | ||
<span class="badge-wishlist"><a href="https://bevry.me/wishlist" title="Buy an item on our wishlist for us"><img src="https://img.shields.io/badge/wishlist-donate-yellow.svg" alt="Wishlist browse button" /></a></span> | ||
@@ -139,0 +148,0 @@ <h3>Contributors</h3> |
@@ -0,21 +1,103 @@ | ||
/** | ||
* Options for formatting the rendered outputs. | ||
* Defaults differ for each output. | ||
* Not all options are relevant on all outputs. | ||
*/ | ||
export interface FormatOptions { | ||
/** Whether or not to display the fellow's email */ | ||
displayEmail?: true | ||
/** A string to proceed each entry */ | ||
prefix?: string | ||
/** Whether or not to display {@link Fellow.url} */ | ||
displayUrl?: boolean | ||
/** Whether or not to display {@link Fellow.description} */ | ||
displayDescription?: boolean | ||
/** Whether or not to display {@link Fellow.email} */ | ||
displayEmail?: boolean | ||
/** Whether or not to display the copright icon */ | ||
displayCopyright?: boolean | ||
/** Whether or not to display the copyright years */ | ||
/** Whether or not to display {@link Fellow.years} */ | ||
displayYears?: boolean | ||
/** Whether or not to display a link to the user's contributions, if used along with {@link .githubRepoSlug} */ | ||
/** Whether or not to display a link to the user's contributions, if used along with {@link FormatOptions.githubRepoSlug} */ | ||
displayContributions?: boolean | ||
/** The repository for when using with {@link .displayContributions} */ | ||
/** The repository for when using with {@link FormatOptions.displayContributions} */ | ||
githubRepoSlug?: string | ||
/** An array of fields to prefer for the URL */ | ||
urlFields?: ['githubUrl', 'url'] | ||
urlFields?: Array<string> | ||
} | ||
/** GitHub Sponsors URL */ | ||
function getUsernameFromGitHubSponsorsUrl(url: string): string { | ||
const match = /^https?:\/\/github\.com\/sponsors\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** GitHub URL */ | ||
function getUsernameFromGitHubUrl(url: string): string { | ||
const match = /^https?:\/\/github\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** Gist URL */ | ||
function getUsernameFromGistUrl(url: string): string { | ||
const match = /^https?:\/\/gist\.github\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** GitLab URL */ | ||
function getUsernameFromGitLabUrl(url: string): string { | ||
const match = /^https?:\/\/gitlab\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** ThanksDev GitHub URL */ | ||
function getGitHubUsernameFromThanksDevUrl(url: string): string { | ||
const match = /^https?:\/\/thanks\.dev\/d\/gh\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** ThanksDev GitLab URL */ | ||
function getGitLabUsernameFromThanksDevUrl(url: string): string { | ||
const match = /^https?:\/\/thanks\.dev\/d\/gl\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** Facebook URL */ | ||
function getUsernameFromFacebookUrl(url: string): string { | ||
const match = /^https?:\/\/facebook\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** Twitter URL */ | ||
function getUsernameFromTwitterUrl(url: string): string { | ||
const match = /^https?:\/\/twitter\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** Patreon URL */ | ||
function getUsernameFromPatreonUrl(url: string): string { | ||
const match = /^https?:\/\/patreon\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** OpenCollective URL */ | ||
function getUsernameFromOpenCollectiveUrl(url: string): string { | ||
const match = /^https?:\/\/opencollective\.com\/([^/]+)\/?$/.exec(url) | ||
return (match && match[1]) || '' | ||
} | ||
/** Trim a value if it is a string */ | ||
function trim(value: any): typeof value { | ||
if (typeof value === 'string') { | ||
return value.trim() | ||
} | ||
return value | ||
} | ||
/** Comparator for sorting fellows in an array */ | ||
@@ -34,179 +116,381 @@ export function comparator(a: Fellow, b: Fellow) { | ||
/** Actual name that is stored, otherwise falls back to username from the url fields */ | ||
private _name?: string | ||
/** A singleton attached to the class that stores it's instances to enable convergence of data */ | ||
static readonly fellows: Array<Fellow> = [] | ||
/** Years active for the current repository, extracted from the name */ | ||
public years?: string | ||
// ----------------------------------- | ||
// Username and Name | ||
/** URLs used */ | ||
readonly urls = new Set<string>() | ||
/** GitHub Username */ | ||
githubUsername: string = '' | ||
/** Emails used */ | ||
readonly emails = new Set<string>() | ||
/** GitLab Username */ | ||
gitlabUsername: string = '' | ||
/** Map of repository slugs with the contributions from the user */ | ||
readonly contributions = new Map<string, number>() | ||
/** Twitter Username */ | ||
twitterUsername: string = '' | ||
/** Set of repository slugs that the fellow administers to */ | ||
readonly administeredRepositories = new Set<string>() | ||
/** Facebook Username */ | ||
facebookUsername: string = '' | ||
/** Set of repository slugs that the fellow contributes to */ | ||
readonly contributedRepositories = new Set<string>() | ||
/** OpenCollective Username */ | ||
opencollectiveUsername: string = '' | ||
/** Set of repository slugs that the fellow maintains */ | ||
readonly maintainedRepositories = new Set<string>() | ||
/** Patreon Username */ | ||
patreonUsername: string = '' | ||
/** Set of repository slugs that the fellow authors */ | ||
readonly authoredRepositories = new Set<string>() | ||
/** Fields used to resolve {@link Fellow.username} */ | ||
protected readonly usernameFields = [ | ||
'githubUsername', | ||
'gitlabUsername', | ||
'twitterUsername', | ||
'facebookUsername', | ||
'opencollectiveUsername', | ||
'patreonUsername', | ||
] | ||
/** Get all unique resolved usernames */ | ||
get usernames(): Array<string> { | ||
return this.getFields(this.usernameFields) | ||
} | ||
/** Get the first resolved {@link Fellow.usernameFields} that is truthy. */ | ||
get username() { | ||
return this.getFirstField(this.usernameFields) || '' | ||
} | ||
/** Years active for the current repository, extracted from the name */ | ||
public years: string = '' | ||
/** Storage of the Nomen (e.g. `Ben`, or `Benjamin Lupton`, but not `balupton`) */ | ||
private _nomen: string = '' | ||
/** Get the resolved Nomen */ | ||
get nomen() { | ||
// clear if not actually a nomen | ||
if (this.usernames.includes(this._nomen)) { | ||
this._nomen = '' | ||
} | ||
// return | ||
return this._nomen | ||
} | ||
/** | ||
* An array of field names that are used to determine if two fellow's are the same. | ||
* Don't need to add usernames for github, twitter, and facebook, as they will be compared via `urls`. | ||
* Can't compare just username, as that is not unique unless comparison's are on the same service, hence why `urls` are used and not `usernames`. | ||
* If the input is prefixed with a series of numbers, that is considered the year: | ||
* E.g. Given `2015+ Bevry Pty Ltd` then `2015+` is the years | ||
* E.g. Given `2013-2015 Bevry Pty Ltd` then `2013-2015` is the years | ||
*/ | ||
protected readonly idFields = ['urls', 'emails'] | ||
set nomen(input: string) { | ||
const match = /^((?:[0-9]+[-+]?)+)?(.+)$/.exec(input) | ||
if (match) { | ||
// fetch the years, but for now, discard it | ||
const years = String(match[1] || '').trim() | ||
if (years) this.years = years | ||
// fetch the name | ||
const name = trim(match[2]) | ||
// apply if actually a nomen | ||
if (this.usernames.includes(name) === false) this._nomen = name | ||
} | ||
} | ||
/** An array of field names that are used to determine the fellow's URL */ | ||
protected readonly urlFields = [ | ||
'url', | ||
'homepage', | ||
'web', | ||
'githubUrl', | ||
'twitterUrl', | ||
'facebookUrl', | ||
] | ||
/** Get {@link Follow.nomen} if resolved, otherwise {@link Fellow.username} */ | ||
get name(): string { | ||
return this.nomen || this.username || '' | ||
} | ||
/** Alias for {@link Fellow.nomen} */ | ||
set name(input: string) { | ||
this.nomen = input | ||
} | ||
/** A singleton attached to the class that stores it's instances to enable convergence of data */ | ||
static readonly fellows: Array<Fellow> = [] | ||
// ----------------------------------- | ||
// Methods | ||
// URLs | ||
/** | ||
* Sort a list of fellows. | ||
* Uses {@link Fellow::sort} for the comparison. | ||
*/ | ||
static sort(list: Array<Fellow>) { | ||
return list.sort(comparator) | ||
/** Storage of the Website URL */ | ||
private _websiteUrl: string = '' | ||
/** Get the resolved Website URL. Used by GitHub GraphQL API. */ | ||
get websiteUrl() { | ||
return this._websiteUrl | ||
} | ||
/** Alias for {@link Fellow.url} */ | ||
set websiteUrl(input: string) { | ||
this.url = input | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by npm. Used by prior Fellow versions. */ | ||
get homepage() { | ||
return this.websiteUrl | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by npm. Used by prior Fellow versions. */ | ||
set homepage(input: string) { | ||
this.websiteUrl = input | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub GraphQL API. */ | ||
get blog() { | ||
return this.websiteUrl | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub REST API. */ | ||
set blog(input: string) { | ||
this.websiteUrl = input | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub GraphQL API. */ | ||
/* eslint-disable-next-line camelcase */ | ||
get html_url() { | ||
return this.websiteUrl | ||
} | ||
/** Alias for {@link Fellow.websiteUrl}. Used by GitHub REST API. */ | ||
/* eslint-disable-next-line camelcase */ | ||
set html_url(input: string) { | ||
this.websiteUrl = input | ||
} | ||
/** Flatten lists of fellows into one set of fellows */ | ||
static flatten(lists: Array<Array<Fellow> | Set<Fellow>>): Set<Fellow> { | ||
const fellows = new Set<Fellow>() | ||
for (const list of lists) { | ||
for (const fellow of list) { | ||
fellows.add(fellow) | ||
} | ||
/** Get the GitHub URL from the {@link Fellow.githubUsername} */ | ||
get githubUrl() { | ||
return this.githubUsername | ||
? `https://github.com/${this.githubUsername}` | ||
: '' | ||
} | ||
/** Set the GitHub URL and username from an input */ | ||
set githubUrl(input: string) { | ||
const username = | ||
getUsernameFromGitHubSponsorsUrl(input) || | ||
getUsernameFromGitHubUrl(input) || | ||
getUsernameFromGistUrl(input) || | ||
getGitHubUsernameFromThanksDevUrl(input) | ||
if (username) { | ||
this.githubUsername = username | ||
} else if (input.includes('github.com')) { | ||
// it is probably something like: https://github.com/apps/dependabot | ||
// ignore as it is not a person | ||
} else { | ||
throw new Error(`Invalid GitHub URL: ${input}`) | ||
} | ||
return fellows | ||
} | ||
/** Compare to another fellow for sorting. */ | ||
compare(other: Fellow): -1 | 0 | 1 { | ||
const A = this.name.toLowerCase() | ||
const B = other.name.toLowerCase() | ||
if (A === B) { | ||
return 0 | ||
} else if (A < B) { | ||
return -1 | ||
/** Get the GitLab URL from the {@link Fellow.gitlabUsername} */ | ||
get gitlabUrl() { | ||
return this.gitlabUsername | ||
? `https://gitlab.com/${this.gitlabUsername}` | ||
: '' | ||
} | ||
/** Set the GitLab URL and username from an input */ | ||
set gitlabUrl(input: string) { | ||
const username = | ||
getUsernameFromGitLabUrl(input) || | ||
getGitLabUsernameFromThanksDevUrl(input) | ||
if (username) { | ||
this.gitlabUsername = username | ||
} else { | ||
return 1 | ||
throw new Error(`Invalid GitLab URL: ${input}`) | ||
} | ||
} | ||
/** | ||
* Compare to another fellow for equivalancy. | ||
* Uses {@link Fellow::idFields} for the comparison. | ||
* @param other The other fellow to compare ourselves with | ||
* @returns Returns `true` if they appear to be the same person, or `false` if not. | ||
*/ | ||
same(other: Fellow): boolean { | ||
for (const field of this.idFields) { | ||
const value = this[field] | ||
const otherValue = other[field] | ||
/** Get the Facebook URL from the {@link Fellow.twitterUsername} */ | ||
get twitterUrl() { | ||
return this.twitterUsername | ||
? `https://twitter.com/${this.twitterUsername}` | ||
: '' | ||
} | ||
/** Set the Twitter URL and username from an input */ | ||
set twitterUrl(input: string) { | ||
const username = getUsernameFromTwitterUrl(input) | ||
if (username) { | ||
this.twitterUsername = username | ||
} else { | ||
throw new Error(`Invalid Twitter URL: ${input}`) | ||
} | ||
} | ||
if (value && otherValue) { | ||
if (value instanceof Set && otherValue instanceof Set) { | ||
for (const item of value) { | ||
if (otherValue.has(item)) { | ||
return true | ||
} | ||
} | ||
} else if (Array.isArray(value) && Array.isArray(otherValue)) { | ||
for (const item of value) { | ||
if (otherValue.includes(item)) { | ||
return true | ||
} | ||
} | ||
} else if (value === otherValue) { | ||
return true | ||
} | ||
/** Get the Facebook URL from the {@link Fellow.facebookUsername} */ | ||
get facebookUrl() { | ||
return this.facebookUsername | ||
? `https://facebook.com/${this.facebookUsername}` | ||
: '' | ||
} | ||
/** Set the Facebook URL and username from an input */ | ||
set facebookUrl(input: string) { | ||
const username = getUsernameFromFacebookUrl(input) | ||
if (username) { | ||
this.facebookUsername = username | ||
} else { | ||
throw new Error(`Invalid Facebook URL: ${input}`) | ||
} | ||
} | ||
/** Get the Patreon URL from the {@link Fellow.patreonUsername} */ | ||
get patreonUrl() { | ||
return this.patreonUsername | ||
? `https://patreon.com/${this.patreonUsername}` | ||
: '' | ||
} | ||
/** Set the Patreon URL and username from an input */ | ||
set patreonUrl(input: string) { | ||
const username = getUsernameFromPatreonUrl(input) | ||
if (username) { | ||
this.patreonUsername = username | ||
} else { | ||
throw new Error(`Invalid Patreon URL: ${input}`) | ||
} | ||
} | ||
/** Get the OpenCollective URL from the {@link Fellow.opencollectiveUsername} */ | ||
get opencollectiveUrl() { | ||
return this.opencollectiveUsername | ||
? `https://opencollective.com/${this.opencollectiveUsername}` | ||
: '' | ||
} | ||
/** Set the OpenCollective URL and username from an input */ | ||
set opencollectiveUrl(input: string) { | ||
const username = getUsernameFromOpenCollectiveUrl(input) | ||
if (username) { | ||
this.opencollectiveUsername = username | ||
} else { | ||
throw new Error(`Invalid OpenCollective URL: ${input}`) | ||
} | ||
} | ||
/** Get the ThanksDev URL from the {@link Fellow.githubUsername} or {@link Fellow.gitlabUsername} */ | ||
get thanksdevUrl() { | ||
return this.githubUsername | ||
? `https://thanks.dev/d/gh/${this.githubUsername}` | ||
: this.gitlabUsername | ||
? `https://thanks.dev/d/gl/${this.gitlabUsername}` | ||
: '' | ||
} | ||
/** Set the ThanksDev URL and username from an input */ | ||
set thanksdevUrl(input: string) { | ||
const githubUsername = getGitHubUsernameFromThanksDevUrl(input) | ||
if (githubUsername) { | ||
this.githubUsername = githubUsername | ||
} else { | ||
const gitlabUsername = getGitLabUsernameFromThanksDevUrl(input) | ||
if (gitlabUsername) { | ||
this.gitlabUsername = gitlabUsername | ||
} else { | ||
throw new Error(`Invalid ThanksDev URL: ${input}`) | ||
} | ||
} | ||
return false | ||
} | ||
/** | ||
* With the value, see if an existing fellow exists in our singleton list property with the value, otherwise create a new fellow instance with the value and add them to our singleton list. | ||
* Uses {@link Fellow::same} for the comparison. | ||
* @param value The value to create a new fellow instance or find the existing fellow instance with | ||
* @param add Whether to add the created person to the list | ||
* @returns The new or existing fellow instance | ||
*/ | ||
static ensure(value: any, add: boolean = true): Fellow { | ||
const newFellow = this.create(value) | ||
for (const existingFellow of this.fellows) { | ||
if (newFellow.same(existingFellow)) { | ||
return existingFellow.set(value) | ||
/** URL fields used to resolve {@link Fellow.url} */ | ||
protected readonly urlFields = [ | ||
'websiteUrl', | ||
'githubUrl', | ||
'gitlabUrl', | ||
'twitterUrl', | ||
'facebookUrl', | ||
'patreonUrl', | ||
'opencollectiveUrl', | ||
'thanksdevUrl', | ||
] | ||
/** Get all unique resolved URLs */ | ||
get urls() { | ||
return this.getFields(this.urlFields) | ||
} | ||
/** Get the first resolved {@link Fellow.urlFields}. Used by GitHub GraphQL API. */ | ||
get url() { | ||
return this.getFirstField(this.urlFields) || '' | ||
} | ||
/** Set the appropriate {@link Fellow.urlFields} from the input */ | ||
set url(input: string) { | ||
input = trim(input) | ||
if (input) { | ||
// convert to https | ||
input = input.replace(/^http:\/\//, 'https://') | ||
// slice 1 to skip websiteUrl | ||
for (const field of this.urlFields) { | ||
// skip websiteUrl in any order, as that is our fallback | ||
if (field === 'websiteUrl') continue | ||
// attempt application of the field | ||
try { | ||
this[field] = input | ||
// the application was successful, it is not a websiteUrl | ||
return | ||
} catch (err: any) { | ||
// the application failed, try the next field | ||
continue | ||
} | ||
} | ||
// all non-websiteUrl applications failed, it must be a websiteUrl | ||
this._websiteUrl = input | ||
} | ||
if (add) { | ||
this.fellows.push(newFellow) | ||
return newFellow | ||
} else { | ||
throw new Error(`Fellow by ${value} does not exist`) | ||
} | ||
// ----------------------------------- | ||
// Emails | ||
/** Emails used */ | ||
readonly emails = new Set<string>() | ||
/** Fetch the first email that was applied, otherwise an empty string */ | ||
get email() { | ||
for (const email of this.emails) { | ||
return email | ||
} | ||
return '' | ||
} | ||
/** | ||
* Get a fellow from the singleton list | ||
* @param value The value to fetch the value with | ||
* @returns The fetched fellow, if they exist with that value | ||
*/ | ||
static get(value: any): Fellow { | ||
return this.ensure(value, false) | ||
/** Add the email to the set instead of replacing it */ | ||
set email(input) { | ||
input = trim(input) | ||
if (input) { | ||
this.emails.add(input) | ||
} | ||
} | ||
// ----------------------------------- | ||
// Description | ||
/** Storage of the description */ | ||
_description: string = '' | ||
/** Get the resolved description */ | ||
get description() { | ||
return this._description | ||
} | ||
/** Set the resolved description */ | ||
set description(input: string) { | ||
input = trim(input) | ||
this._description = input | ||
} | ||
/** Alias for {@link Fellow.description} */ | ||
get bio() { | ||
return this.description | ||
} | ||
/** Alias for {@link Fellow.description} */ | ||
set bio(input: string) { | ||
this.description = input | ||
} | ||
// ----------------------------------- | ||
// Identification | ||
/** | ||
* Add a fellow or a series of people, denoted by the value, to the singleton list | ||
* @param values The fellow or people to add | ||
* @returns An array of the fellow objects for the passed people | ||
* An array of field names that are used to determine if two fellow's are the same. | ||
* Don't need to add usernames for github, twitter, and facebook, as they will be compared via `urls`. | ||
* Can't compare just username, as that is not unique unless comparison's are on the same service, hence why `urls` are used and not `usernames`. | ||
*/ | ||
static add(...values: any[]): Array<Fellow> { | ||
const result: Array<Fellow> = [] | ||
for (const value of values) { | ||
if (value instanceof this) { | ||
result.push(this.ensure(value)) | ||
} else if (typeof value === 'string') { | ||
result.push(...value.split(/, +/).map((fellow) => this.ensure(fellow))) | ||
} else if (Array.isArray(value)) { | ||
result.push(...value.map((value) => this.ensure(value))) | ||
} else if (value) { | ||
result.push(this.ensure(value)) | ||
protected readonly idFields = ['urls', 'emails'] | ||
/** An array of identifiers, all lowercased to prevent typestrong/TypeStrong double-ups */ | ||
get ids() { | ||
const results = new Set<string>() | ||
for (const field of this.idFields) { | ||
const value = this[field] | ||
if (value instanceof Set || Array.isArray(value)) { | ||
for (const item of value) { | ||
results.add(item.toLowerCase()) | ||
} | ||
} else { | ||
return String(value).toLowerCase() | ||
} | ||
} | ||
return result | ||
return Array.from(results.values()).filter(Boolean) | ||
} | ||
/** Create a new Fellow instance with the value, however if the value is already a fellow instance, then just return it */ | ||
static create(value: any) { | ||
return value instanceof this ? value : new this(value) | ||
} | ||
// ----------------------------------- | ||
// Methods | ||
/** | ||
* Construct our fellow instance with the value | ||
* @param value The value used to set the properties of the fellow, forwarded to {@link Fellow::set} | ||
* @param input The value used to set the properties of the fellow, forwarded to {@link Fellow.set} | ||
*/ | ||
constructor(value: any) { | ||
this.set(value) | ||
constructor(input: any) { | ||
this.set(input) | ||
} | ||
@@ -226,5 +510,5 @@ | ||
} | ||
const name = (match[1] || '').trim() | ||
const email = (match[2] || '').trim() | ||
const url = (match[3] || '').trim() | ||
const name = trim(match[1]) | ||
const email = trim(match[2]) | ||
const url = trim(match[3]) | ||
if (name) this.name = name | ||
@@ -239,16 +523,7 @@ if (email) this.email = email | ||
if (key[0] === '_') return // skip if private | ||
const value = fellow[key] || null | ||
if (value) { | ||
// if any of the url fields, redirect to url setter | ||
if (this.urlFields.includes(key)) { | ||
this.url = value | ||
} | ||
// if not a url field, e.g. name or email | ||
else { | ||
this[key] = value | ||
} | ||
} | ||
const value = trim(fellow[key]) | ||
if (value) this[key] = value | ||
}) | ||
} else { | ||
throw new Error('Invalid fellow input') | ||
throw new Error(`Invalid Fellow input: ${JSON.stringify(fellow)}`) | ||
} | ||
@@ -259,140 +534,224 @@ | ||
// ----------------------------------- | ||
// Accessors | ||
/** Compare to another fellow for sorting. */ | ||
compare(other: Fellow): -1 | 0 | 1 { | ||
const a = this.name.toLowerCase() | ||
const b = other.name.toLowerCase() | ||
if (a === b) { | ||
return 0 | ||
} else if (a < b) { | ||
return -1 | ||
} else { | ||
return 1 | ||
} | ||
} | ||
/** | ||
* If the name is empty, we will try to fallback to githubUsername then twitterUsername | ||
* If the name is prefixed with a series of numbers, that is considered the year | ||
* E.g. In `2015+ Bevry Pty Ltd` then `2015+` is the years | ||
* E.g. In `2013-2015 Bevry Pty Ltd` then `2013-2015` is the years | ||
* Compare to another fellow for equivalency. | ||
* Uses {@link Fellow.ids} for the comparison. | ||
* @param other The other fellow to compare ourselves with | ||
* @returns Returns `true` if they appear to be the same person, or `false` if not. | ||
*/ | ||
set name(value /* :string */) { | ||
const match = /^((?:[0-9]+[-+]?)+)?(.+)$/.exec(value) | ||
if (match) { | ||
// fetch the years, but for now, discard it | ||
const years = String(match[1] || '').trim() | ||
if (years) this.years = years | ||
// fetch the name, and apply it | ||
const name = match[2].trim() | ||
if (name) this._name = name | ||
same(other: Fellow): boolean { | ||
const ids = new Set(this.ids) | ||
const otherIds = new Set(other.ids) | ||
for (const id of ids) { | ||
if (otherIds.has(id)) { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
// ----------------------------------- | ||
// Static | ||
/** | ||
* Fetch the user's name, otherwise their usernames | ||
* Sort a list of fellows. | ||
* Uses {@link Fellow.compare} for the comparison. | ||
*/ | ||
get name(): string { | ||
return ( | ||
this._name || | ||
this.githubUsername || | ||
this.twitterUsername || | ||
this.facebookUsername || | ||
'' | ||
) | ||
static sort(list: Array<Fellow> | Set<Fellow>) { | ||
if (list instanceof Set) { | ||
list = Array.from(list.values()) | ||
} | ||
list = list.sort(comparator) | ||
return list | ||
} | ||
/** Add the email to the set instead of replacing it */ | ||
set email(value) { | ||
if (value) { | ||
this.emails.add(value) | ||
/** Flatten lists of fellows into one set of fellows */ | ||
static flatten(lists: Array<Array<Fellow> | Set<Fellow>>): Set<Fellow> { | ||
const fellows = new Set<Fellow>() | ||
for (const list of lists) { | ||
for (const fellow of list) { | ||
fellows.add(fellow) | ||
} | ||
} | ||
return fellows | ||
} | ||
/** Fetch the first email that was applied, otherwise an empty string */ | ||
get email() { | ||
for (const email of this.emails) { | ||
return email | ||
/** | ||
* With the value, see if an existing fellow exists in our singleton list property with the value, otherwise create a new fellow instance with the value and add them to our singleton list. | ||
* Uses {@link Fellow.same} for the comparison. | ||
* @param input The value to create a new fellow instance or find the existing fellow instance with | ||
* @param add Whether to add the created person to the list | ||
* @returns The new or existing fellow instance | ||
*/ | ||
static ensure(input: any, add: boolean = true): Fellow { | ||
if (input instanceof Fellow && this.fellows.includes(input)) return input | ||
const newFellow = this.create(input) | ||
for (const existingFellow of this.fellows) { | ||
if (newFellow.same(existingFellow)) { | ||
return existingFellow.set(input) | ||
} | ||
} | ||
return '' | ||
if (add) { | ||
this.fellows.push(newFellow) | ||
return newFellow | ||
} else { | ||
throw new Error(`Fellow does not exist: ${input}`) | ||
} | ||
} | ||
/** | ||
* Will determine if the passed URL is a github, facebook, or twitter URL. | ||
* If it is, then it will extract the username and url from it. | ||
* If it was not, then it will set the homepage variable. | ||
* Get a fellow from the singleton list | ||
* @param input The value to fetch the value with | ||
* @returns The fetched fellow, if they exist with that value | ||
*/ | ||
set url(input: string) { | ||
if (input) { | ||
let url: string | ||
// github | ||
const githubMatch = /^.+github.com\/([^/]+)\/?$/.exec(input) | ||
if (githubMatch) { | ||
this.githubUsername = githubMatch[1] | ||
url = this.githubUrl = 'https://github.com/' + this.githubUsername | ||
} else { | ||
const facebookMatch = /^.+facebook.com\/([^/]+)\/?$/.exec(input) | ||
if (facebookMatch) { | ||
this.facebookUsername = facebookMatch[1] | ||
url = this.facebookUrl = | ||
'https://facebook.com/' + this.facebookUsername | ||
} else { | ||
const twitterMatch = /^.+twitter.com\/([^/]+)\/?$/.exec(input) | ||
if (twitterMatch) { | ||
this.twitterUsername = twitterMatch[1] | ||
url = this.twitterUrl = | ||
'https://twitter.com/' + this.twitterUsername | ||
} else { | ||
url = this.homepage = input | ||
} | ||
static get(input: any): Fellow { | ||
return this.ensure(input, false) | ||
} | ||
/** | ||
* Add a fellow or a series of people, denoted by the value, to the singleton list | ||
* @param inputs The fellow or people to add | ||
* @returns A de-duplicated array of fellow objects for the passed people | ||
*/ | ||
static add(...inputs: any[]): Array<Fellow> { | ||
const list = new Set<Fellow>() | ||
for (const input of inputs) { | ||
if (input instanceof this) { | ||
list.add(this.ensure(input)) | ||
} else if (typeof input === 'string') { | ||
for (const item of input.split(/, +/)) { | ||
list.add(this.ensure(item)) | ||
} | ||
} else if (input instanceof Set || Array.isArray(input)) { | ||
for (const item of input) { | ||
list.add(this.ensure(item)) | ||
} | ||
} else if (input) { | ||
list.add(this.ensure(input)) | ||
} | ||
// add url in encrypted and unecrypted forms to urls | ||
this.urls.add(url.replace(/^http:/, 'https:')) | ||
this.urls.add(url.replace(/^https:/, 'http:')) | ||
} | ||
return Array.from(list.values()) | ||
} | ||
/** Fetch the homepage with fallback to one of the service URLs if available */ | ||
get url() { | ||
return ( | ||
this.homepage || | ||
this.githubUrl || | ||
this.facebookUrl || | ||
this.twitterUrl || | ||
'' | ||
/** Create a new Fellow instance with the value, however if the value is already a fellow instance, then just return it */ | ||
static create(value: any) { | ||
return value instanceof this ? value : new this(value) | ||
} | ||
// ----------------------------------- | ||
// Repositories | ||
/** Set of GitHub repository slugs that the fellow authors */ | ||
readonly authorOfRepositories = new Set<string>() | ||
/** Get all fellows who author a particular GitHub repository */ | ||
static authorsOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.authorOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
/** Get the field field from the list that isn't empty */ | ||
getFirstField(fields: string[]) { | ||
for (const field of fields) { | ||
const value = this[field] | ||
if (value) return value | ||
} | ||
return null | ||
/** Set of GitHub repository slugs that the fellow maintains */ | ||
readonly maintainerOfRepositories = new Set<string>() | ||
/** Get all fellows who maintain a particular GitHub repository */ | ||
static maintainersOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.maintainerOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
// ----------------------------------- | ||
// Repositories | ||
/** Map of GitHub repository slugs to the contribution count of the user */ | ||
readonly contributionsOfRepository = new Map<string, number>() | ||
/** Get all fellows who administrate a particular repository */ | ||
static administersRepository(repoSlug: string): Array<Fellow> { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.administeredRepositories.has(repoSlug) | ||
}) | ||
/** Set of GitHub repository slugs that the fellow contributes to */ | ||
readonly contributorOfRepositories = new Set<string>() | ||
/** Get all fellows who contribute to a particular GitHub repository */ | ||
static contributorsOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.contributorOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
/** Get all fellows who contribute to a particular repository */ | ||
static contributesRepository(repoSlug: string): Array<Fellow> { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.contributedRepositories.has(repoSlug) | ||
}) | ||
/** Set of GitHub repository slugs that the fellow initially financed */ | ||
readonly funderOfRepositories = new Set<string>() | ||
/** Get all fellows who initally financed a particular GitHub repository */ | ||
static fundersOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.funderOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
/** Get all fellows who maintain a particular repository */ | ||
static maintainsRepository(repoSlug: string): Array<Fellow> { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.maintainedRepositories.has(repoSlug) | ||
}) | ||
/** Set of GitHub repository slugs that the fellow actively finances */ | ||
readonly sponsorOfRepositories = new Set<string>() | ||
/** Get all fellows who actively finance a particular GitHub repository */ | ||
static sponsorsOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.sponsorOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
/** Get all fellows who author a particular repository */ | ||
static authorsRepository(repoSlug: string): Array<Fellow> { | ||
return this.fellows.filter(function (fellow) { | ||
return fellow.authoredRepositories.has(repoSlug) | ||
}) | ||
/** Set of GitHub repository slugs that the fellow has historically financed */ | ||
readonly donorOfRepositories = new Set<string>() | ||
/** Get all fellows who have historically financed a particular GitHub repository */ | ||
static donorsOfRepository(repoSlug: string): Array<Fellow> { | ||
return this.sort( | ||
this.fellows.filter(function (fellow) { | ||
return fellow.donorOfRepositories.has(repoSlug) | ||
}) | ||
) | ||
} | ||
// @todo figure out how to calculate this | ||
// /** Map of GitHub repository slugs to the sponsorship amount of the user */ | ||
// readonly sponsorships = new Map<string, number>() | ||
// ----------------------------------- | ||
// Formats | ||
// Formatting | ||
/** Get the first field from the list that isn't empty */ | ||
getFirstField(fields: string[]) { | ||
for (const field of fields) { | ||
const value = this[field] | ||
if (value) return value | ||
} | ||
return null | ||
} | ||
/** Get the all the de-duplicated fields from the list that aren't empty */ | ||
getFields(fields: string[]) { | ||
const set = new Set<string>() | ||
for (const field of fields) { | ||
const value = this[field] | ||
if (value) set.add(value) | ||
} | ||
return Array.from(set.values()) | ||
} | ||
/** | ||
@@ -414,3 +773,3 @@ * Convert the fellow into the usual string format | ||
if (format.displayEmail && this.email) { | ||
if (format.displayEmail !== false && this.email) { | ||
parts.push(`<${this.email}>`) | ||
@@ -432,2 +791,35 @@ } | ||
/** | ||
* Convert the fellow into the usual text format | ||
* @example `NAME 📝 DESCRIPTION 🔗 URL` | ||
*/ | ||
toText(format: FormatOptions = {}): string { | ||
if (!this.name) return '' | ||
const parts = [] | ||
// prefix | ||
if (format.prefix) parts.push(format.prefix) | ||
// name | ||
parts.push(`${this.name}`) | ||
if (format.displayEmail && this.email) { | ||
parts.push(`✉️ ${this.email}`) | ||
} | ||
// description | ||
if (format.displayDescription !== false && this.description) { | ||
parts.push(`📝 ${this.description}`) | ||
} | ||
// url | ||
if (format.displayUrl !== false && this.url) { | ||
parts.push(`🔗 ${this.url}`) | ||
} | ||
// return | ||
return parts.join(' ') | ||
} | ||
/** | ||
* Convert the fellow into the usual markdown format | ||
@@ -462,5 +854,5 @@ * @example `[NAME](URL) <EMAIL>` | ||
) { | ||
const contributionsURL = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}` | ||
const contributionsUrl = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}` | ||
parts.push( | ||
`— [view contributions](${contributionsURL} "View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}")`, | ||
`— [view contributions](${contributionsUrl} "View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}")` | ||
) | ||
@@ -494,3 +886,3 @@ } | ||
parts.push( | ||
`<a href="mailto:${this.email}" title="Email ${this.name}"><${this.email}></a>`, | ||
`<a href="mailto:${this.email}" title="Email ${this.name}"><${this.email}></a>` | ||
) | ||
@@ -505,5 +897,5 @@ } | ||
) { | ||
const contributionsURL = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}` | ||
const contributionsUrl = `https://github.com/${format.githubRepoSlug}/commits?author=${this.githubUsername}` | ||
parts.push( | ||
`— <a href="${contributionsURL}" title="View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}">view contributions</a>`, | ||
`— <a href="${contributionsUrl}" title="View the GitHub contributions of ${this.name} on repository ${format.githubRepoSlug}">view contributions</a>` | ||
) | ||
@@ -510,0 +902,0 @@ } |
{ | ||
"compilerOptions": { | ||
"allowJs": true, | ||
"downlevelIteration": true, | ||
"esModuleInterop": true, | ||
"isolatedModules": true, | ||
"maxNodeModuleJsDepth": 5, | ||
"module": "ESNext", | ||
"moduleResolution": "Node", | ||
"strict": true, | ||
"target": "ES2017", | ||
"module": "ESNext" | ||
"target": "ES2022" | ||
}, | ||
"include": ["source"] | ||
} |
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
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
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
250284
18
20
5963
163
1
1
1
+ Addededitions@^6.19.0
+ Addededitions@6.21.0(transitive)
+ Addedversion-range@4.14.0(transitive)