Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

fellow

Package Overview
Dependencies
Maintainers
1
Versions
118
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

fellow - npm Package Compare versions

Comparing version 6.25.0 to 7.0.0-next.1703075602.d3e1a4be5066f75a2b484559e0c4f5b5a631a61b

edition-es2016/index.js

834

edition-browsers/index.js

@@ -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

// email
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}`);
// email
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

// email
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}`)
// email
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}">&lt;${this.email}&gt;</a>`,
`<a href="mailto:${this.email}" title="Email ${this.name}">&lt;${this.email}&gt;</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 @@ }

{
"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&amp;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

// email
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}`)
// email
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}">&lt;${this.email}&gt;</a>`,
`<a href="mailto:${this.email}" title="Email ${this.name}">&lt;${this.email}&gt;</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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc