refactor!: move dirs outside of src/ (#2032)

This commit is contained in:
Skillz4Killz
2022-02-11 09:49:53 +00:00
committed by GitHub
parent 471ef5cb6c
commit 8aaea9f339
594 changed files with 84 additions and 66 deletions
+7
View File
@@ -0,0 +1,7 @@
export function snowflakeToBigint(snowflake: string) {
return BigInt(snowflake) | 0n;
}
export function bigintToSnowflake(snowflake: bigint) {
return snowflake === 0n ? "" : snowflake.toString();
}
+7
View File
@@ -0,0 +1,7 @@
import { GatewayManager } from "../ws/gateway_manager.ts";
export function calculateShardId(gateway: GatewayManager, guildId: bigint) {
if (gateway.maxShards === 1) return 0;
return Number((guildId >> 22n) % BigInt(gateway.maxShards - 1));
}
+144
View File
@@ -0,0 +1,144 @@
import { Bot } from "../bot.ts";
export class Collection<K, V> extends Map<K, V> {
maxSize?: number;
sweeper?: CollectionSweeper<K, V> & { intervalId?: number };
constructor(entries?: (readonly (readonly [K, V])[] | null) | Map<K, V>, options?: CollectionOptions<K, V>) {
super(entries ?? []);
this.maxSize = options?.maxSize;
if (!options?.sweeper) return;
this.startSweeper(options.sweeper);
}
startSweeper(options: CollectionSweeper<K, V>): number {
if (this.sweeper?.intervalId) clearInterval(this.sweeper.intervalId);
this.sweeper = options;
this.sweeper.intervalId = setInterval(() => {
this.forEach((value, key) => {
if (!this.sweeper?.filter(value, key, options.bot)) return;
this.delete(key);
return key;
});
}, options.interval);
return this.sweeper.intervalId!;
}
stopSweeper(): void {
return clearInterval(this.sweeper?.intervalId);
}
changeSweeperInterval(newInterval: number) {
if (!this.sweeper) return;
this.startSweeper({ filter: this.sweeper.filter, interval: newInterval });
}
changeSweeperFilter(newFilter: (value: V, key: K, bot: Bot) => boolean) {
if (!this.sweeper) return;
this.startSweeper({ filter: newFilter, interval: this.sweeper.interval });
}
set(key: K, value: V) {
// When this collection is maxSizeed make sure we can add first
if ((this.maxSize || this.maxSize === 0) && this.size >= this.maxSize) {
return this;
}
return super.set(key, value);
}
array() {
return [...this.values()];
}
/** Retrieve the value of the first element in this collection */
first(): V | undefined {
return this.values().next().value;
}
last(): V | undefined {
return [...this.values()][this.size - 1];
}
random(): V | undefined {
const array = [...this.values()];
return array[Math.floor(Math.random() * array.length)];
}
find(callback: (value: V, key: K) => boolean) {
for (const key of this.keys()) {
const value = this.get(key)!;
if (callback(value, key)) return value;
}
// If nothing matched
return;
}
filter(callback: (value: V, key: K) => boolean) {
const relevant = new Collection<K, V>();
this.forEach((value, key) => {
if (callback(value, key)) relevant.set(key, value);
});
return relevant;
}
map<T>(callback: (value: V, key: K) => T) {
const results = [];
for (const key of this.keys()) {
const value = this.get(key)!;
results.push(callback(value, key));
}
return results;
}
some(callback: (value: V, key: K) => boolean) {
for (const key of this.keys()) {
const value = this.get(key)!;
if (callback(value, key)) return true;
}
return false;
}
every(callback: (value: V, key: K) => boolean) {
for (const key of this.keys()) {
const value = this.get(key)!;
if (!callback(value, key)) return false;
}
return true;
}
reduce<T>(callback: (accumulator: T, value: V, key: K) => T, initialValue?: T): T {
let accumulator: T = initialValue!;
for (const key of this.keys()) {
const value = this.get(key)!;
accumulator = callback(accumulator, value, key);
}
return accumulator;
}
}
export interface CollectionOptions<K, V> {
sweeper?: CollectionSweeper<K, V>;
maxSize?: number;
}
export interface CollectionSweeper<K, V> {
/** The filter to determine whether an element should be deleted or not */
filter: (value: V, key: K, ...args: any[]) => boolean;
/** The interval in which the sweeper should run */
interval: number;
/** The bot object itself */
bot?: Bot;
}
+177
View File
@@ -0,0 +1,177 @@
/** https://discord.com/developers/docs/reference#api-reference-base-url */
export const BASE_URL = "https://discord.com/api";
/** https://discord.com/developers/docs/reference#api-versioning-api-versions */
export const API_VERSION = 9;
/** https://discord.com/developers/docs/topics/gateway#gateways-gateway-versions */
export const GATEWAY_VERSION = 9;
// TODO: update this version
/** https://github.com/discordeno/discordeno/releases */
export const DISCORDENO_VERSION = "13.0.0-rc19";
/** https://discord.com/developers/docs/reference#user-agent */
export const USER_AGENT = `DiscordBot (https://github.com/discordeno/discordeno, v${DISCORDENO_VERSION})`;
/** https://discord.com/developers/docs/reference#image-formatting-image-base-url */
export const IMAGE_BASE_URL = "https://cdn.discordapp.com";
// This can be modified by big brain bots and use a proxy
export const baseEndpoints = {
BASE_URL: `${BASE_URL}/v${API_VERSION}`,
CDN_URL: IMAGE_BASE_URL,
};
const GUILDS_BASE = (guildId: bigint) => `${baseEndpoints.BASE_URL}/guilds/${guildId}`;
const CHANNEL_BASE = (channelId: bigint) => `${baseEndpoints.BASE_URL}/channels/${channelId}`;
export const endpoints = {
GUILDS_BASE,
CHANNEL_BASE,
GATEWAY_BOT: `${baseEndpoints.BASE_URL}/gateway/bot`,
// Channel Endpoints
CHANNEL_MESSAGE: (channelId: bigint, messageId: bigint) => `${CHANNEL_BASE(channelId)}/messages/${messageId}`,
CHANNEL_MESSAGES: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/messages`,
CHANNEL_PIN: (channelId: bigint, messageId: bigint) => `${CHANNEL_BASE(channelId)}/pins/${messageId}`,
CHANNEL_PINS: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/pins`,
CHANNEL_BULK_DELETE: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/messages/bulk-delete`,
CHANNEL_INVITES: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/invites`,
CHANNEL_WEBHOOKS: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/webhooks`,
CHANNEL_MESSAGE_REACTION_ME: (channelId: bigint, messageId: bigint, emoji: string) =>
`${CHANNEL_BASE(channelId)}/messages/${messageId}/reactions/${emoji}/@me`,
CHANNEL_MESSAGE_REACTION_USER: (channelId: bigint, messageId: bigint, emoji: string, userId: bigint) =>
`${CHANNEL_BASE(channelId)}/messages/${messageId}/reactions/${emoji}/${userId}`,
CHANNEL_MESSAGE_REACTIONS: (channelId: bigint, messageId: bigint) =>
`${CHANNEL_BASE(channelId)}/messages/${messageId}/reactions`,
CHANNEL_MESSAGE_REACTION: (channelId: bigint, messageId: bigint, emoji: string) =>
`${CHANNEL_BASE(channelId)}/messages/${messageId}/reactions/${emoji}`,
CHANNEL_FOLLOW: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/followers`,
CHANNEL_MESSAGE_CROSSPOST: (channelId: bigint, messageId: bigint) =>
`${CHANNEL_BASE(channelId)}/messages/${messageId}/crosspost`,
CHANNEL_OVERWRITE: (channelId: bigint, overwriteId: bigint) =>
`${CHANNEL_BASE(channelId)}/permissions/${overwriteId}`,
// Bots SHALL NOT use this endpoint but they can
CHANNEL_TYPING: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/typing`,
// Thread Endpoints
THREAD_START_PUBLIC: (channelId: bigint, messageId: bigint) =>
`${endpoints.CHANNEL_MESSAGE(channelId, messageId)}/threads`,
THREAD_START_PRIVATE: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/threads`,
THREAD_ACTIVE: (guildId: bigint) => `${GUILDS_BASE(guildId)}/threads/active`,
THREAD_MEMBERS: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/thread-members`,
THREAD_ME: (channelId: bigint) => `${endpoints.THREAD_MEMBERS(channelId)}/@me`,
THREAD_USER: (channelId: bigint, userId: bigint) => `${endpoints.THREAD_MEMBERS(channelId)}/${userId}`,
THREAD_ARCHIVED_BASE: (channelId: bigint) => `${CHANNEL_BASE(channelId)}/threads/archived`,
THREAD_ARCHIVED_PUBLIC: (channelId: bigint) => `${endpoints.THREAD_ARCHIVED_BASE(channelId)}/public`,
THREAD_ARCHIVED_PRIVATE: (channelId: bigint) => `${endpoints.THREAD_ARCHIVED_BASE(channelId)}/private`,
THREAD_ARCHIVED_PRIVATE_JOINED: (channelId: bigint) =>
`${CHANNEL_BASE(channelId)}/users/@me/threads/archived/private`,
// Guild Endpoints
GUILDS: `${baseEndpoints.BASE_URL}/guilds`,
GUILD_AUDIT_LOGS: (guildId: bigint) => `${GUILDS_BASE(guildId)}/audit-logs`,
GUILD_BAN: (guildId: bigint, userId: bigint) => `${GUILDS_BASE(guildId)}/bans/${userId}`,
GUILD_BANS: (guildId: bigint) => `${GUILDS_BASE(guildId)}/bans`,
GUILD_BANNER: (guildId: bigint, icon: string) => `${baseEndpoints.CDN_URL}/banners/${guildId}/${icon}`,
GUILD_CHANNELS: (guildId: bigint) => `${GUILDS_BASE(guildId)}/channels`,
GUILD_WIDGET: (guildId: bigint) => `${GUILDS_BASE(guildId)}/widget`,
GUILD_EMOJI: (guildId: bigint, emojiId: bigint) => `${GUILDS_BASE(guildId)}/emojis/${emojiId}`,
GUILD_EMOJIS: (guildId: bigint) => `${GUILDS_BASE(guildId)}/emojis`,
GUILD_ICON: (guildId: bigint, icon: string) => `${baseEndpoints.CDN_URL}/icons/${guildId}/${icon}`,
GUILD_INTEGRATION: (guildId: bigint, integrationId: bigint) =>
`${GUILDS_BASE(guildId)}/integrations/${integrationId}`,
GUILD_INTEGRATION_SYNC: (guildId: bigint, integrationId: bigint) =>
`${GUILDS_BASE(guildId)}/integrations/${integrationId}/sync`,
GUILD_INTEGRATIONS: (guildId: bigint) => `${GUILDS_BASE(guildId)}/integrations?include_applications=true`,
GUILD_INVITES: (guildId: bigint) => `${GUILDS_BASE(guildId)}/invites`,
GUILD_LEAVE: (guildId: bigint) => `${baseEndpoints.BASE_URL}/users/@me/guilds/${guildId}`,
GUILD_MEMBER: (guildId: bigint, userId: bigint) => `${GUILDS_BASE(guildId)}/members/${userId}`,
GUILD_MEMBERS: (guildId: bigint) => `${GUILDS_BASE(guildId)}/members`,
GUILD_MEMBER_ROLE: (guildId: bigint, memberId: bigint, roleId: bigint) =>
`${GUILDS_BASE(guildId)}/members/${memberId}/roles/${roleId}`,
GUILD_MEMBERS_SEARCH: (guildId: bigint) => `${GUILDS_BASE(guildId)}/members/search`,
GUILD_PRUNE: (guildId: bigint) => `${GUILDS_BASE(guildId)}/prune`,
GUILD_REGIONS: (guildId: bigint) => `${GUILDS_BASE(guildId)}/regions`,
GUILD_ROLE: (guildId: bigint, roleId: bigint) => `${GUILDS_BASE(guildId)}/roles/${roleId}`,
GUILD_ROLES: (guildId: bigint) => `${GUILDS_BASE(guildId)}/roles`,
GUILD_SPLASH: (guildId: bigint, icon: string) => `${baseEndpoints.CDN_URL}/splashes/${guildId}/${icon}`,
GUILD_VANITY_URL: (guildId: bigint) => `${GUILDS_BASE(guildId)}/vanity-url`,
GUILD_WEBHOOKS: (guildId: bigint) => `${GUILDS_BASE(guildId)}/webhooks`,
GUILD_TEMPLATE: (code: string) => `${baseEndpoints.BASE_URL}/guilds/templates/${code}`,
GUILD_TEMPLATES: (guildId: bigint) => `${GUILDS_BASE(guildId)}/templates`,
GUILD_PREVIEW: (guildId: bigint) => `${GUILDS_BASE(guildId)}/preview`,
UPDATE_VOICE_STATE: (guildId: bigint, userId?: bigint) => `${GUILDS_BASE(guildId)}/voice-states/${userId ?? "@me"}`,
GUILD_WELCOME_SCREEN: (guildId: bigint) => `${GUILDS_BASE(guildId)}/welcome-screen`,
GUILD_SCHEDULED_EVENTS: (guildId: bigint) => `${GUILDS_BASE(guildId)}/scheduled-events`,
GUILD_SCHEDULED_EVENT: (guildId: bigint, eventId: bigint) => `${GUILDS_BASE(guildId)}/scheduled-events/${eventId}`,
GUILD_SCHEDULED_EVENT_USERS: (guildId: bigint, eventId: bigint) =>
`${GUILDS_BASE(guildId)}/scheduled-events/${eventId}/users`,
// Voice
VOICE_REGIONS: `${baseEndpoints.BASE_URL}/voice/regions`,
INVITE: (inviteCode: string) => `${baseEndpoints.BASE_URL}/invites/${inviteCode}`,
WEBHOOK: (webhookId: bigint, token: string) => `${baseEndpoints.BASE_URL}/webhooks/${webhookId}/${token}`,
WEBHOOK_ID: (webhookId: bigint) => `${baseEndpoints.BASE_URL}/webhooks/${webhookId}`,
WEBHOOK_MESSAGE: (webhookId: bigint, token: string, messageId: bigint) =>
`${baseEndpoints.BASE_URL}/webhooks/${webhookId}/${token}/messages/${messageId}`,
WEBHOOK_MESSAGE_ORIGINAL: (webhookId: bigint, token: string) =>
`${baseEndpoints.BASE_URL}/webhooks/${webhookId}/${token}/messages/@original`,
WEBHOOK_SLACK: (webhookId: bigint, token: string) => `${baseEndpoints.BASE_URL}/webhooks/${webhookId}/${token}/slack`,
WEBHOOK_GITHUB: (webhookId: bigint, token: string) =>
`${baseEndpoints.BASE_URL}/webhooks/${webhookId}/${token}/github`,
// Application Endpoints
COMMANDS: (applicationId: bigint) => `${baseEndpoints.BASE_URL}/applications/${applicationId}/commands`,
COMMANDS_GUILD: (applicationId: bigint, guildId: bigint) =>
`${baseEndpoints.BASE_URL}/applications/${applicationId}/guilds/${guildId}/commands`,
COMMANDS_PERMISSIONS: (applicationId: bigint, guildId: bigint) =>
`${endpoints.COMMANDS_GUILD(applicationId, guildId)}/permissions`,
COMMANDS_PERMISSION: (applicationId: bigint, guildId: bigint, commandId: bigint) =>
`${endpoints.COMMANDS_GUILD(applicationId, guildId)}/${commandId}/permissions`,
COMMANDS_ID: (applicationId: bigint, commandId: bigint) =>
`${baseEndpoints.BASE_URL}/applications/${applicationId}/commands/${commandId}`,
COMMANDS_GUILD_ID: (applicationId: bigint, guildId: bigint, commandId: bigint) =>
`${baseEndpoints.BASE_URL}/applications/${applicationId}/guilds/${guildId}/commands/${commandId}`,
// Interaction Endpoints
INTERACTION_ID_TOKEN: (interactionId: bigint, token: string) =>
`${baseEndpoints.BASE_URL}/interactions/${interactionId}/${token}/callback`,
INTERACTION_ORIGINAL_ID_TOKEN: (interactionId: bigint, token: string) =>
`${baseEndpoints.BASE_URL}/webhooks/${interactionId}/${token}/messages/@original`,
INTERACTION_ID_TOKEN_MESSAGE_ID: (applicationId: bigint, token: string, messageId: bigint) =>
`${baseEndpoints.BASE_URL}/webhooks/${applicationId}/${token}/messages/${messageId}`,
// User endpoints
USER: (userId: bigint) => `${baseEndpoints.BASE_URL}/users/${userId}`,
USER_BOT: `${baseEndpoints.BASE_URL}/users/@me`,
USER_GUILDS: `${baseEndpoints.BASE_URL}/@me/guilds`,
USER_AVATAR: (userId: bigint, icon: string) => `${baseEndpoints.CDN_URL}/avatars/${userId}/${icon}`,
USER_DEFAULT_AVATAR: (icon: number) => `${baseEndpoints.CDN_URL}/embed/avatars/${icon}.png`,
USER_DM: `${baseEndpoints.BASE_URL}/users/@me/channels`,
USER_CONNECTIONS: `${baseEndpoints.BASE_URL}/users/@me/connections`,
USER_NICK: (guildId: bigint) => `${GUILDS_BASE(guildId)}/members/@me`,
// Discovery Endpoints
DISCOVERY_CATEGORIES: `${baseEndpoints.BASE_URL}/discovery/categories`,
DISCOVERY_VALID_TERM: `${baseEndpoints.BASE_URL}/discovery/valid-term`,
DISCOVERY_METADATA: (guildId: bigint) => `${GUILDS_BASE(guildId)}/discovery-metadata`,
DISCOVERY_SUBCATEGORY: (guildId: bigint, categoryId: number) =>
`${GUILDS_BASE(guildId)}/discovery-categories/${categoryId}`,
// OAuth2
OAUTH2_APPLICATION: `${baseEndpoints.BASE_URL}/oauth2/applications/@me`,
// Stage instances
STAGE_INSTANCES: `${baseEndpoints.BASE_URL}/stage-instances`,
STAGE_INSTANCE: (channelId: bigint) => `${baseEndpoints.BASE_URL}/stage-instances/${channelId}`,
};
export const SLASH_COMMANDS_NAME_REGEX = /^[\w-]{1,32}$/;
export const CONTEXT_MENU_COMMANDS_NAME_REGEX = /^[\w-\s]{1,32}$/;
export const CHANNEL_MENTION_REGEX = /<#[0-9]+>/g;
export const DISCORD_SNOWFLAKE_REGEX = /^(?<id>\d{17,19})$/;
+19
View File
@@ -0,0 +1,19 @@
export function iconHashToBigInt(hash: string) {
// The icon is animated so it needs special handling
if (hash.startsWith("a_")) {
// Change the `a_` to just be `a`
hash = `a${hash.substring(2)}`;
} else {
// The icon is not animated but it could be that it starts with a 0 so we just put a `b` in front so nothing breaks
hash = `b${hash}`;
}
return BigInt(`0x${hash}`);
}
export function iconBigintToHash(icon: bigint) {
// Convert the bigint back to a hash
const hash = icon.toString(16);
// Hashes starting with a are animated and with b are not so need to handle that
return hash.startsWith("a") ? `a_${hash.substring(1)}` : hash.substring(1);
}
+7
View File
@@ -0,0 +1,7 @@
export * from "./bigint.ts";
export * from "./calculateShardId.ts";
export * from "./collection.ts";
export * from "./constants.ts";
export * from "./hash.ts";
export * from "./utils.ts";
export * from "./validateLength.ts";
+22
View File
@@ -0,0 +1,22 @@
import { BitwisePermissionFlags } from "../types/permissions/bitwisePermissionFlags.ts";
import { PermissionStrings } from "../types/permissions/permissionStrings.ts";
/** This function converts a bitwise string to permission strings */
export function calculatePermissions(permissionBits: bigint) {
return Object.keys(BitwisePermissionFlags).filter((permission) => {
// Since Object.keys() not only returns the permission names but also the bit values we need to return false if it is a Number
if (Number(permission)) return false;
// Check if permissionBits has this permission
return permissionBits & BigInt(BitwisePermissionFlags[permission as PermissionStrings]);
}) as PermissionStrings[];
}
/** This function converts an array of permissions into the bitwise string. */
export function calculateBits(permissions: PermissionStrings[]) {
return permissions
.reduce((bits, perm) => {
bits |= BigInt(BitwisePermissionFlags[perm]);
return bits;
}, 0n)
.toString();
}
+111
View File
@@ -0,0 +1,111 @@
/** Converts a url to base 64. Useful for example, uploading/creating server emojis. */
export async function urlToBase64(url: string) {
const buffer = await fetch(url).then((res) => res.arrayBuffer());
const imageStr = encode(buffer);
const type = url.substring(url.lastIndexOf(".") + 1);
return `data:image/${type};base64,${imageStr}`;
}
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.
const base64abc = [
"A",
"B",
"C",
"D",
"E",
"F",
"G",
"H",
"I",
"J",
"K",
"L",
"M",
"N",
"O",
"P",
"Q",
"R",
"S",
"T",
"U",
"V",
"W",
"X",
"Y",
"Z",
"a",
"b",
"c",
"d",
"e",
"f",
"g",
"h",
"i",
"j",
"k",
"l",
"m",
"n",
"o",
"p",
"q",
"r",
"s",
"t",
"u",
"v",
"w",
"x",
"y",
"z",
"0",
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"+",
"/",
];
/**
* CREDIT: https://gist.github.com/enepomnyaschih/72c423f727d395eeaa09697058238727
* Encodes a given Uint8Array, ArrayBuffer or string into RFC4648 base64 representation
* @param data
*/
export function encode(data: ArrayBuffer | string): string {
const uint8 = typeof data === "string"
? new TextEncoder().encode(data)
: data instanceof Uint8Array
? data
: new Uint8Array(data);
let result = "",
i;
const l = uint8.length;
for (i = 2; i < l; i += 3) {
result += base64abc[uint8[i - 2] >> 2];
result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
result += base64abc[((uint8[i - 1] & 0x0f) << 2) | (uint8[i] >> 6)];
result += base64abc[uint8[i] & 0x3f];
}
if (i === l + 1) {
// 1 octet yet to write
result += base64abc[uint8[i - 2] >> 2];
result += base64abc[(uint8[i - 2] & 0x03) << 4];
result += "==";
}
if (i === l) {
// 2 octets yet to write
result += base64abc[uint8[i - 2] >> 2];
result += base64abc[((uint8[i - 2] & 0x03) << 4) | (uint8[i - 1] >> 4)];
result += base64abc[(uint8[i - 1] & 0x0f) << 2];
result += "=";
}
return result;
}
+28
View File
@@ -0,0 +1,28 @@
import type { ImageFormat } from "../types/misc/imageFormat.ts";
import type { ImageSize } from "../types/misc/imageSize.ts";
/** Pause the execution for a given amount of milliseconds. */
export function delay(ms: number): Promise<void> {
return new Promise((res): number =>
setTimeout((): void => {
res();
}, ms)
);
}
/** Help format an image url. */
export const formatImageURL = (url: string, size: ImageSize = 128, format?: ImageFormat) => {
return `${url}.${format || (url.includes("/a_") ? "gif" : "jpg")}?size=${size}`;
};
// Typescript is not so good as we developers so we need this little utility function to help it out
// Taken from https://fettblog.eu/typescript-hasownproperty/
/** TS save way to check if a property exists in an object */
// deno-lint-ignore ban-types
export function hasProperty<T extends {}, Y extends PropertyKey = string>(
obj: T,
prop: Y,
): obj is T & Record<Y, unknown> {
// deno-lint-ignore no-prototype-builtins
return obj.hasOwnProperty(prop);
}
+11
View File
@@ -0,0 +1,11 @@
/** Validates the length of a string in JS. Certain characters in JS can have multiple numbers in length in unicode and discords api is in python which treats length differently. */
export function validateLength(text: string, options: { max?: number; min?: number }) {
const length = [...text].length;
// Text is too long
if (options.max && length > options.max) return false;
// Text is too short
if (options.min && length < options.min) return false;
return true;
}