@jdlien/validator 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,656 @@
1
+ /**
2
+ * Utilities used by the Validator class.
3
+ *
4
+ * @format
5
+ */
6
+
7
+ type DateParts = { year: number; month: number; day: number }
8
+
9
+ export function isFormControl(el: any): boolean {
10
+ return (
11
+ el instanceof HTMLInputElement ||
12
+ el instanceof HTMLSelectElement ||
13
+ el instanceof HTMLTextAreaElement
14
+ )
15
+ }
16
+
17
+ interface ValidationResult {
18
+ valid: boolean
19
+ error?: boolean
20
+ messages: string[]
21
+ }
22
+
23
+ // Checks if an element has a type or data-type attribute matching
24
+ // one of the types in the passed array
25
+ export function isType(
26
+ el: HTMLInputElement | HTMLTextAreaElement,
27
+ types: string | string[]
28
+ ): boolean {
29
+ if (typeof types === 'string') types = [types]
30
+
31
+ const dataType = el.dataset.type || ''
32
+ const type = el.type || ''
33
+
34
+ if (types.includes(dataType)) return true
35
+ if (types.includes(type)) return true
36
+ return false
37
+ }
38
+
39
+ // Converts moment.js-style formats strings to the flatpickr format
40
+ // https://flatpickr.js.org/formatting/
41
+ // Useful to use FlatPickr in conjunction with Validator
42
+ // Not comprehensive but covers common format strings
43
+ export function momentToFPFormat(format: string) {
44
+ return format
45
+ .replace(/YYYY/g, 'Y')
46
+ .replace(/YY/g, 'y')
47
+ .replace(/MMMM/g, 'F')
48
+ .replace(/MMM/g, '{3}')
49
+ .replace(/MM/g, '{2}')
50
+ .replace(/M/g, 'n')
51
+ .replace(/DD/g, '{5}')
52
+ .replace(/D/g, 'j')
53
+ .replace(/dddd/g, 'l')
54
+ .replace(/ddd/g, 'D')
55
+ .replace(/dd/g, 'D')
56
+ .replace(/d/g, 'w')
57
+ .replace(/HH/g, '{6}')
58
+ .replace(/H/g, 'G')
59
+ .replace(/hh/g, 'h')
60
+ .replace(/mm/g, 'i')
61
+ .replace(/m/g, 'i')
62
+ .replace(/ss/g, 'S')
63
+ .replace(/s/g, 's')
64
+ .replace(/A/gi, 'K')
65
+ .replace(/\{3\}/g, 'M')
66
+ .replace(/\{2\}/g, 'm')
67
+ .replace(/\{5\}/g, 'd')
68
+ .replace(/\{6\}/g, 'H')
69
+ }
70
+
71
+ // Converts month name to zero-based month number
72
+ export function monthToNumber(str: string | number): number {
73
+ const num = parseInt(str as string)
74
+ if (typeof str === 'number' || !isNaN(num)) return num - 1
75
+
76
+ const m = new Date(`1 ${str} 2000`).getMonth()
77
+ if (!isNaN(m)) return m
78
+
79
+ const dict: { [key: string]: number } = {
80
+ ja: 0,
81
+ en: 0,
82
+ fe: 1,
83
+ fé: 1,
84
+ ap: 3,
85
+ ab: 3,
86
+ av: 3,
87
+ mai: 4,
88
+ juin: 5,
89
+ juil: 6,
90
+ au: 7,
91
+ ag: 7,
92
+ ao: 7,
93
+ se: 8,
94
+ o: 9,
95
+ n: 10,
96
+ d: 11,
97
+ }
98
+
99
+ for (const key in dict) {
100
+ if (str.toLowerCase().startsWith(key)) return dict[key]
101
+ }
102
+
103
+ throw new Error('Invalid month name: ' + str)
104
+ }
105
+
106
+ // Convert two-digit year to a four digit year
107
+ export function yearToFull(year: number | string): number {
108
+ if (typeof year === 'string') year = parseInt(year.replace(/\D/g, ''))
109
+ if (year > 99) return year
110
+ // If year less than 20 years in the future, assume the 21st century
111
+ if (year < (new Date().getFullYear() + 20) % 100) return year + 2000
112
+ return year + 1900
113
+ }
114
+
115
+ // Parses a string and returns the most plausible date
116
+ export function parseDate(value: string | Date): Date {
117
+ if (value instanceof Date) return value
118
+
119
+ value = value.trim().toLowerCase()
120
+
121
+ let year: number = 0
122
+ let month: number = 0
123
+ let day: number = 0
124
+ let hour: number = 0
125
+ let minute: number = 0
126
+ let second: number = 0
127
+
128
+ const timeRE = new RegExp(/\d{1,2}\:\d\d(?:\:\d\ds?)?\s?(?:[a|p]m?)?/gi)
129
+ // If the value contains a time, set the time variables
130
+ if (timeRE.test(value)) {
131
+ const timeStr = value.match(timeRE)![0]
132
+ // Remove the time from the string
133
+ value = value.replace(timeStr, '').trim()
134
+ const timeParts = parseTime(timeStr)
135
+ // Assign the time to the variables
136
+ if (timeParts !== null) ({ hour, minute, second } = timeParts)
137
+
138
+ // If the value seems to be a time only, return a date with this time.
139
+ if (value.length <= 2) {
140
+ const now = new Date()
141
+ return new Date(now.getFullYear(), now.getMonth(), now.getDate(), hour, minute, second)
142
+ }
143
+ }
144
+
145
+ // Strip day of the week from the string in English, French, or Spanish
146
+ const dayOfWeekRegex = /(^|\b)(mo|tu|we|th|fr|sa|su|lu|mard|mer|jeu|ve|dom)[\w]*\.?/gi
147
+ value = value.replace(dayOfWeekRegex, '').trim()
148
+
149
+ // Convert now and today to the current date at midnight
150
+ const today = new Date(new Date().setHours(0, 0, 0, 0))
151
+ if (/(now|today)/.test(value)) return today
152
+ if (value.includes('tomorrow')) return new Date(today.setDate(today.getDate() + 1))
153
+
154
+ // Handle a undelimited 6 or 8-digit number and treat it as YYYYMMDD
155
+ if (value.length === 8) value = value.replace(/(\d\d\d\d)(\d\d)(\d\d)/, '$1-$2-$3')
156
+ if (value.length === 6)
157
+ value = value.replace(/(\d\d)(\d\d)(\d\d)/, yearToFull(value.slice(0, 2)) + '-$2-$3')
158
+
159
+ try {
160
+ ;({ year, month, day } = guessDateParts(value))
161
+ } catch (e) {
162
+ return new Date('')
163
+ }
164
+
165
+ // Return date (month is 0-based)
166
+ return new Date(year, month - 1, day, hour, minute, second)
167
+ } // end parseDate
168
+
169
+ // Returns most likely meanings of each part of a date. Assumes 1-based months
170
+ function guessDateParts(str: string): DateParts {
171
+ // Returns array of possible meanings for a token in a date string
172
+ // Pass an array of known parameters to exclude them from the guess
173
+ // Assumes months are 1-based.
174
+ function guessDatePart(
175
+ num: number,
176
+ knownMeanings: (string | null)[] = [null, null, null]
177
+ ): string[] {
178
+ const unknown = (arr: string[]): string[] => arr.filter((i) => !knownMeanings.includes(i))
179
+
180
+ if (num === 0 || num > 31) return unknown(['y'])
181
+ if (num > 12) return unknown(['d', 'y'])
182
+ if (num >= 1 || num <= 12) return unknown(['m', 'd', 'y'])
183
+ return []
184
+ }
185
+
186
+ const tokens = str.split(/[\s-/:.,]+/).filter((i) => i !== '')
187
+
188
+ // If two tokens, add a year to the beginning
189
+ if (tokens.length < 3) {
190
+ // If one of the tokens is a 4-digit number, don't have enough info to guess the date
191
+ if (str.match(/\d{4}/) !== null) throw new Error('Invalid Date')
192
+ else tokens.unshift(String(new Date().getFullYear()))
193
+ }
194
+
195
+ const date: DateParts = { year: 0, month: 0, day: 0 }
196
+
197
+ function assignPart(part: keyof DateParts, num: number): void {
198
+ if (part === 'year') date.year = yearToFull(num)
199
+ else date[part] = num
200
+ }
201
+
202
+ let count = 0
203
+ while (!(date.year && date.month && date.day)) {
204
+ tokenLoop: for (const token of tokens) {
205
+ count++
206
+ // If word
207
+ if (/^[a-zA-Zé]+$/.test(token)) {
208
+ if (!date.month) assignPart('month', monthToNumber(token) + 1)
209
+ continue
210
+ }
211
+
212
+ // If the token is a year
213
+ if (/^'\d\d$/.test(token) || /^\d{3,5}$/.test(token)) {
214
+ if (!date.year) assignPart('year', parseInt(token.replace(/'/, '')))
215
+ continue
216
+ }
217
+
218
+ // If the token is a number
219
+ const num = parseInt(token)
220
+ if (isNaN(num)) {
221
+ console.error(`not date because ${token} isNaN`)
222
+ throw new Error('Invalid Date')
223
+ }
224
+
225
+ const meanings = guessDatePart(num, [
226
+ date.year ? 'y' : null,
227
+ date.month ? 'm' : null,
228
+ date.day ? 'd' : null,
229
+ ])
230
+
231
+ if (meanings.length == 1) {
232
+ for (let i = 0; i < meanings.length; i++) {
233
+ if (meanings[i] === 'm' && !date.month) {
234
+ assignPart('month', num)
235
+ continue tokenLoop
236
+ }
237
+ if (meanings[i] === 'd' && !date.day) {
238
+ assignPart('day', num)
239
+ continue tokenLoop
240
+ }
241
+ if (meanings[i] === 'y' && !date.year) {
242
+ assignPart('year', num)
243
+ continue tokenLoop
244
+ }
245
+ }
246
+ }
247
+
248
+ // If we have no idea what the token is after going through all of them
249
+ // set token to the first thing it could be starting with month
250
+ if (count > 3) {
251
+ if (!date.month && meanings.includes('m')) assignPart('month', num)
252
+ else if (!date.day && meanings.includes('d')) assignPart('day', num)
253
+ else if (!date.year && meanings.includes('y')) assignPart('year', num)
254
+ }
255
+ }
256
+
257
+ // Should never take more than six iterations to figure out a valid date
258
+ if (count > 6) throw new Error('Invalid Date')
259
+ }
260
+
261
+ if (date.year && date.month && date.day) return date
262
+ throw new Error('Invalid Date')
263
+ }
264
+
265
+ // A simplified version of apps-date.ts's parseTime function.
266
+ export function parseTime(value: string): { hour: number; minute: number; second: number } | null {
267
+ // if "now" or "today" is in the string, return the current time
268
+ value = value.trim().toLowerCase()
269
+ if (value === 'now') {
270
+ const now = new Date()
271
+ return { hour: now.getHours(), minute: now.getMinutes(), second: now.getSeconds() }
272
+ }
273
+
274
+ // If there is a 3-4 digit number, assume it's a time and add a colon
275
+ const timeParts = value.match(/(\d{3,4})/)
276
+
277
+ if (timeParts) {
278
+ const length = timeParts[1].length
279
+ const hour = timeParts[1].slice(0, length == 3 ? 1 : 2)
280
+ const minutes = timeParts[1].slice(-2)
281
+ value = value.replace(timeParts[1], hour + ':' + minutes)
282
+ }
283
+
284
+ // Match a simple time without minutes or seconds and optional am/pm
285
+ const shortTimeRegex = new RegExp(/^(\d{1,2})(?::(\d{1,2}))?\s*(?:(a|p)m?)?$/i)
286
+ if (shortTimeRegex.test(value)) {
287
+ const shortParts = value.match(shortTimeRegex)
288
+ if (shortParts === null) return null
289
+ value = shortParts[1] + ':' + (shortParts[2] || '00') + (shortParts[3] || '')
290
+ }
291
+
292
+ // Regex to match time in 0:0 format with optional seconds and am/pm
293
+ const timeRegex = new RegExp(/^(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?\s*(?:(a|p)m?)?$/i)
294
+
295
+ if (!timeRegex.test(value)) return null
296
+
297
+ const parts = value.match(timeRegex)
298
+ if (parts === null) return null
299
+
300
+ const hour = parseInt(parts[1])
301
+ const minute = parseInt(parts[2])
302
+ const second = parts[3] ? parseInt(parts[3]) : 0
303
+ const ampm = parts[4]
304
+
305
+ if (isNaN(hour) || isNaN(minute) || isNaN(second)) return null
306
+
307
+ if (ampm === 'p' && hour < 12) return { hour: hour + 12, minute, second }
308
+ if (ampm === 'a' && hour === 12) return { hour: 0, minute, second }
309
+
310
+ if (hour < 0 || hour > 23) return null
311
+ if (minute < 0 || minute > 59) return null
312
+ if (second < 0 || second > 59) return null
313
+
314
+ return { hour, minute, second }
315
+ } // parseTime
316
+
317
+ export function parseTimeToString(value: string, format: string = 'h:mm A'): string {
318
+ const time = parseTime(value)
319
+
320
+ if (time) {
321
+ const date = new Date()
322
+ date.setHours(time.hour)
323
+ date.setMinutes(time.minute)
324
+ date.setSeconds(time.second)
325
+ date.setMilliseconds(0)
326
+ return formatDateTime(date, format)
327
+ }
328
+
329
+ return ''
330
+ }
331
+
332
+ // Accepts a date or date-like string and returns a formatted date string
333
+ // Uses moment-compatible format strings
334
+ export function formatDateTime(date: Date | string, format: string = 'YYYY-MM-DD'): string {
335
+ // Ensure the date is a valid date object
336
+ date = parseDate(date) as Date
337
+
338
+ // if date is an invalid date object, return empty string
339
+ if (isNaN(date.getTime())) return ''
340
+
341
+ const d = {
342
+ y: date.getFullYear(),
343
+ M: date.getMonth(),
344
+ D: date.getDate(),
345
+ W: date.getDay(),
346
+ H: date.getHours(),
347
+ m: date.getMinutes(),
348
+ s: date.getSeconds(),
349
+ ms: date.getMilliseconds(),
350
+ }
351
+
352
+ const pad = (n: number | string, w = 2) => (n + '').padStart(w, '0')
353
+
354
+ const getH = () => d.H % 12 || 12
355
+
356
+ const getMeridiem = (hour: number) => (hour < 12 ? 'AM' : 'PM')
357
+
358
+ const monthToString = (month: number) =>
359
+ 'January|February|March|April|May|June|July|August|September|October|November|December'.split(
360
+ '|'
361
+ )[month]
362
+
363
+ function dayToString(day: number, len: number = 0): string {
364
+ const days = 'Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday'.split('|')
365
+ return len ? days[day].slice(0, len) : days[day]
366
+ }
367
+
368
+ const matches: { [key: string]: string | number } = {
369
+ YY: String(d.y).slice(-2),
370
+ YYYY: d.y,
371
+ M: d.M + 1,
372
+ MM: pad(d.M + 1),
373
+ MMMM: monthToString(d.M),
374
+ MMM: monthToString(d.M).slice(0, 3),
375
+ D: String(d.D),
376
+ DD: pad(d.D),
377
+ d: String(d.W),
378
+ dd: dayToString(d.W, 2),
379
+ ddd: dayToString(d.W, 3),
380
+ dddd: dayToString(d.W),
381
+ H: String(d.H),
382
+ HH: pad(d.H),
383
+ h: getH(),
384
+ hh: pad(getH()),
385
+ A: getMeridiem(d.H),
386
+ a: getMeridiem(d.H).toLowerCase(),
387
+ m: String(d.m),
388
+ mm: pad(d.m),
389
+ s: String(d.s),
390
+ ss: pad(d.s),
391
+ SSS: pad(d.ms, 3),
392
+ }
393
+
394
+ return format.replace(
395
+ /\[([^\]]+)]|Y{1,4}|M{1,4}|D{1,2}|d{1,4}|H{1,2}|h{1,2}|a|A|m{1,2}|s{1,2}|Z{1,2}|SSS/g,
396
+ (match, $1) => $1 || matches[match]
397
+ )
398
+ } // end formatDateTime
399
+
400
+ export function parseDateToString(value: string | Date, format?: string): string {
401
+ const date = parseDate(value)
402
+ if (isNaN(date.getTime())) return ''
403
+ // if the format is undefined or has length of 0, set it to the default format
404
+ if (!format || format.length === 0) format = 'YYYY-MMM-DD'
405
+ return formatDateTime(date, format)
406
+ }
407
+
408
+ // Check if a the value of a specified input is a valid date and is in a specified date range
409
+ export function isDate(value: string | Date): boolean {
410
+ let date = parseDate(value)
411
+
412
+ if (date === null || date === undefined) return false
413
+
414
+ return !isNaN(date.getTime())
415
+ }
416
+
417
+ // Check if a date is within the specified range
418
+ export function isDateInRange(date: Date, range: string): boolean {
419
+ if (range === 'past' && date > new Date()) return false
420
+ if (range === 'future' && date.getTime() < new Date().setHours(0, 0, 0, 0)) return false
421
+
422
+ // In the future, may add support for ranges like 'last 30 days' or 'next 3 months'
423
+ // or specific dates like '2019-01-01 to 2019-12-31'
424
+ return true
425
+ }
426
+
427
+ export function isTime(value: string): boolean {
428
+ let timeObj = parseTime(value)
429
+
430
+ if (timeObj === null) return false
431
+
432
+ return !isNaN(timeObj.hour) && !isNaN(timeObj.minute) && !isNaN(timeObj.second)
433
+ }
434
+
435
+ // Input validation for email fields
436
+ export function isEmail(value: string): boolean {
437
+ // Emails cannot be longer than 255 characters
438
+ if (value.length > 255) return false
439
+
440
+ // This is a relatively simple regex just to check that an email has a valid TLD
441
+ // It will not catch all invalid emails, the next regex does that
442
+ let emailTLDRegex = new RegExp(/^.+@.+\.[a-zA-Z0-9]{2,}$/)
443
+ if (!emailTLDRegex.test(value)) return false
444
+
445
+ // A comprehensive regular expression to check for valid emails. Does not allow for unicode characters.
446
+ let re = ''
447
+ // Begin local part. Allow alphanumeric characters and some special characters
448
+ re += "^([a-zA-Z0-9!#$%'*+/=?^_`{|}~-]+"
449
+
450
+ // Allow dot separated sequences of the above characters (representing multiple labels in the local part)
451
+ re += "(?:\\.[a-zA-Z0-9!#$%&'*+/=?^_`{|}~-]+)*"
452
+
453
+ // Allow a quoted string (using either single or double quotes)
454
+ re += '|'
455
+ re +=
456
+ '"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*"'
457
+ // End of local part and begin domain part of email address
458
+ re += ')@('
459
+ // Domain part can be either a sequence of labels, separated by dots, ending with a TLD
460
+ re += '('
461
+ re += '(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\\.)+'
462
+ // TLD must be at least 2 characters long
463
+ re += '[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?'
464
+ re += ')'
465
+ // Or, it can be an address within square brackets but
466
+ // we will not allow this - real people shouldn't be using IP addresses in email addresses
467
+ re += ')$' // End of string
468
+
469
+ let emailRegex = new RegExp(re)
470
+
471
+ return emailRegex.test(value)
472
+ }
473
+
474
+ // Parse a North American Numbering Plan phone number (xxx-xxx-xxxx)
475
+ export function parseNANPTel(value: string): string {
476
+ // first character regex, strip anything that isn't part of the area code
477
+ value = value.replace(/^[^2-90]+/g, '')
478
+ // now the first number should be for the area code
479
+ value = value.replace(/(\d\d\d).*?(\d\d\d).*?(\d\d\d\d)(.*)/, '$1-$2-$3$4')
480
+
481
+ return value
482
+ }
483
+
484
+ // Checks that the phone number is valid in North American Numbering Plan
485
+ export function isNANPTel(value: string): boolean {
486
+ return /^\d\d\d-\d\d\d-\d\d\d\d$/.test(value)
487
+ }
488
+
489
+ export function parseInteger(value: string): string {
490
+ return value.replace(/[^0-9]/g, '')
491
+ }
492
+
493
+ export function isNumber(value: string): boolean {
494
+ return /^\-?\d*\.?\d*$/.test(value)
495
+ }
496
+
497
+ export function parseNumber(value: string): string {
498
+ return value
499
+ .replace(/[^\-0-9.]/g, '') // all but digits, hyphens, and periods
500
+ .replace(/(^-)|(-)/g, (_match, p1) => (p1 ? '-' : '')) // all but the first hyphen
501
+ .replace(/(\..*)\./g, '$1') // periods after a first one
502
+ }
503
+
504
+ export function isInteger(value: string): boolean {
505
+ return /^\-?\d*$/.test(value)
506
+ }
507
+
508
+ // If the string isn't already a valid url, prepends 'https://'
509
+ export function parseUrl(value: string): string {
510
+ value = value.trim()
511
+ const urlRegex = new RegExp('^(?:[a-z+]+:)?//', 'i')
512
+ if (urlRegex.test(value)) return value
513
+ else return 'https://' + value
514
+ }
515
+
516
+ // Checks if this is a valid URL in a protocol agnostic way,
517
+ // allows for protocol-relative absolute URLs (eg //example.com)
518
+ export function isUrl(value: string): boolean {
519
+ const urlRegex = new RegExp('^(?:[-a-z+]+:)?//', 'i')
520
+ return urlRegex.test(value)
521
+ }
522
+
523
+ export function parseZip(value: string): string {
524
+ value = value
525
+ .replace(/[^0-9]/g, '')
526
+ .replace(/(.{5})(.*)/, '$1-$2')
527
+ .trim()
528
+
529
+ // If the zip code has 6 characters, remove a hyphen
530
+ if (value.length === 6) value = value.replace(/-/, '')
531
+
532
+ return value
533
+ }
534
+
535
+ export function isZip(value: string): boolean {
536
+ const zipRegex = new RegExp(/^\d{5}(-\d{4})?$/)
537
+ return zipRegex.test(value)
538
+ }
539
+
540
+ export function parsePostalCA(value: string): string {
541
+ value = value
542
+ .toUpperCase()
543
+ .replace(/[^A-Z0-9]/g, '')
544
+ .replace(/(.{3})\s*(.*)/, '$1 $2')
545
+ .trim()
546
+ return value
547
+ }
548
+
549
+ export function isPostalCA(value: string): boolean {
550
+ const postalRegex = new RegExp(
551
+ /^[ABCEGHJKLMNPRSTVXY][0-9][ABCEGHJKLMNPRSTVWXYZ] ?[0-9][ABCEGHJKLMNPRSTVWXYZ][0-9]$/
552
+ )
553
+ return postalRegex.test(value)
554
+ }
555
+
556
+ // Checks if the value is a valid CSS color
557
+ // Falls back to a regex if CSS.supports isn't available
558
+ export function isColor(value: string): boolean {
559
+ if (['transparent', 'currentColor'].includes(value)) return true
560
+
561
+ if (typeof value !== 'string' || !value.trim()) return false
562
+
563
+ if (typeof CSS === 'object' && typeof CSS.supports === 'function') {
564
+ return CSS.supports('color', value)
565
+ }
566
+
567
+ // If CSS.supports isn't available, use regexes to check for valid rgb or hsl color values
568
+ // Not as comprehensive as the CSS.supports method, but should work in older browsers
569
+ return isColorRegex(value)
570
+ }
571
+
572
+ function isColorRegex(value: string): boolean {
573
+ const rgbRegex = new RegExp(
574
+ /^rgba?\(\s*(\d{1,3}%?,\s*){2}\d{1,3}%?\s*(?:,\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/
575
+ )
576
+
577
+ const hslRegex = new RegExp(
578
+ /^hsla?\(\s*\d+(deg|grad|rad|turn)?,\s*\d{1,3}%,\s*\s*\d{1,3}%(?:,\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/
579
+ )
580
+
581
+ // Support for the newer space-separated syntax
582
+ const rgbSpaceRegex = new RegExp(
583
+ /^rgba?\(\s*(\d{1,3}%?\s+){2}\d{1,3}%?\s*(?:\s*\/\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/
584
+ )
585
+
586
+ const hslSpaceRegex = new RegExp(
587
+ /^hsla?\(\s*\d+(deg|grad|rad|turn)?\s+\d{1,3}%\s+\s*\d{1,3}%(?:\s*\/\s*(\.\d+|0+(\.\d+)?|1(\.0+)?|0|1\.0|\d{1,2}(\.\d*)?%|100%))?\s*\)$/
588
+ )
589
+
590
+ // Hex color regex (short and long formats with and without alpha)
591
+ const hexRegex = new RegExp(/^#([0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i)
592
+
593
+ let colors = `aliceblue|antiquewhite|aqua|aquamarine|azure|beige|bisque|black|blanchedalmond|blue|blueviolet|brown|burlywood|cadetblue|chartreuse|chocolate|coral|cornflowerblue|cornsilk|crimson|cyan|darkblue|darkcyan|darkgoldenrod|darkgray|darkgreen|darkgrey|darkkhaki|darkmagenta|darkolivegreen|darkorange|darkorchid|darkred|darksalmon|darkseagreen|darkslateblue|darkslategray|darkslategrey|darkturquoise|darkviolet|deeppink|deepskyblue|dimgray|dimgrey|dodgerblue|firebrick|floralwhite|forestgreen|fuchsia|gainsboro|ghostwhite|gold|goldenrod|gray|green|greenyellow|grey|honeydew|hotpink|indianred|indigo|ivory|khaki|lavender|lavenderblush|lawngreen|lemonchiffon|lightblue|lightcoral|lightcyan|lightgoldenrodyellow|lightgray|lightgreen|lightgrey|lightpink|lightsalmon|lightseagreen|lightskyblue|lightslategray|lightslategrey|lightsteelblue|lightyellow|lime|limegreen|linen|magenta|maroon|mediumaquamarine|mediumblue|mediumorchid|mediumpurple|mediumseagreen|mediumslateblue|mediumspringgreen|mediumturquoise|mediumvioletred|midnightblue|mintcream|mistyrose|moccasin|navajowhite|navy|oldlace|olive|olivedrab|orange|orangered|orchid|palegoldenrod|palegreen|paleturquoise|palevioletred|papayawhip|peachpuff|peru|pink|plum|powderblue|purple|rebeccapurple|red|rosybrown|royalblue|saddlebrown|salmon|sandybrown|seagreen|seashell|sienna|silver|skyblue|slateblue|slategray|slategrey|snow|springgreen|steelblue|tan|teal|thistle|tomato|turquoise|violet|wheat|white|whitesmoke|yellow|yellowgreen`
594
+
595
+ const colorNameRegex = new RegExp(`^(${colors})$`, 'i')
596
+
597
+ return (
598
+ rgbRegex.test(value) ||
599
+ hslRegex.test(value) ||
600
+ rgbSpaceRegex.test(value) ||
601
+ hslSpaceRegex.test(value) ||
602
+ hexRegex.test(value) ||
603
+ colorNameRegex.test(value)
604
+ )
605
+ }
606
+
607
+ // Used to convert color names to hex values
608
+ let colorCanvas: HTMLCanvasElement | null = null
609
+
610
+ // Cache of color names to hex values to reduce reads to canvas
611
+ const colorCache = new Map<string, string>()
612
+
613
+ // Uses a Canvas element to convert a color name to a hex value
614
+ export function parseColor(value: string): string {
615
+ value = value.trim().toLowerCase()
616
+
617
+ if (value === 'transparent') return 'transparent'
618
+ if (colorCache.has(value)) return colorCache.get(value)!
619
+
620
+ if (colorCanvas === null) {
621
+ colorCanvas = document.createElement('canvas')
622
+ // May improve performance (or suppress warnings) but I don't think it does much
623
+ ;(<any>colorCanvas).willReadFrequently = true
624
+ }
625
+ let ctx = colorCanvas.getContext('2d')!
626
+ if (!ctx) throw new Error("Can't get context from colorCanvas")
627
+
628
+ ctx!.fillStyle = value
629
+ ctx!.fillRect(0, 0, 1, 1)
630
+ let d = ctx!.getImageData(0, 0, 1, 1).data
631
+ let hex = '#' + ('000000' + ((d[0] << 16) | (d[1] << 8) | d[2]).toString(16)).slice(-6)
632
+
633
+ colorCache.set(value, hex)
634
+
635
+ return hex
636
+ }
637
+
638
+ // Homogenizes the return of a custom validation function to a ValidationResult
639
+ // that has a boolean valid property and messages array of strings
640
+ export function normalizeValidationResult(
641
+ res:
642
+ | boolean
643
+ | string
644
+ | { valid: boolean; message?: string; messages?: string | string[]; error?: boolean }
645
+ ): ValidationResult {
646
+ let result: ValidationResult = { valid: false, error: false, messages: [] }
647
+ if (typeof res === 'boolean') return { valid: res, error: false, messages: [] }
648
+ if (typeof res === 'string') return { valid: false, error: false, messages: [res] }
649
+ if (typeof res.valid === 'boolean') result.valid = res.valid
650
+ if (typeof res.message === 'string') result.messages = [res.message]
651
+ if (typeof res.messages === 'string') result.messages = [res.messages]
652
+ if (Array.isArray(res.messages)) result.messages = res.messages
653
+ if (res.error === true) result.error = true
654
+
655
+ return result
656
+ }
@@ -0,0 +1,12 @@
1
+ const forms = require('@tailwindcss/forms')
2
+
3
+ module.exports = {
4
+ content: [
5
+ '!./node_modules/**',
6
+ './**/*.html',
7
+ './src/*.ts',
8
+ './src/*.js',
9
+ './*.svg',
10
+ ],
11
+ plugins: [forms],
12
+ }