@ditojs/utils 2.100.0 → 3.1.0

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.
Files changed (48) hide show
  1. package/package.json +5 -9
  2. package/src/array/shuffle.js +1 -1
  3. package/src/string/formatPlainDate.js +14 -0
  4. package/src/string/index.js +2 -0
  5. package/src/string/isDomain.js +10 -3
  6. package/src/string/parsePlainDate.js +18 -0
  7. package/types/index.d.ts +13 -0
  8. package/src/__snapshots__/index.test.js.snap +0 -88
  9. package/src/array/flattten.test.js +0 -29
  10. package/src/array/shuffle.test.js +0 -25
  11. package/src/base/base.test.js +0 -590
  12. package/src/class/mixin.test.js +0 -70
  13. package/src/dataPath/getEntriesAtDataPath.test.js +0 -204
  14. package/src/dataPath/getValueAtDataPath.test.js +0 -140
  15. package/src/dataPath/normalizeDataPath.test.js +0 -36
  16. package/src/dataPath/parseDataPath.test.js +0 -67
  17. package/src/dataPath/setDataPathEntries.test.js +0 -36
  18. package/src/dataPath/setValueAtDataPath.test.js +0 -34
  19. package/src/function/toAsync.test.js +0 -31
  20. package/src/function/toCallback.test.js +0 -29
  21. package/src/function/toPromiseCallback.test.js +0 -24
  22. package/src/html/escapeHtml.test.js +0 -19
  23. package/src/html/stripHtml.test.js +0 -37
  24. package/src/index.test.js +0 -7
  25. package/src/object/clone.test.js +0 -131
  26. package/src/object/equals.test.js +0 -278
  27. package/src/object/groupBy.test.js +0 -70
  28. package/src/object/mapKeys.test.js +0 -16
  29. package/src/object/mapValues.test.js +0 -38
  30. package/src/object/mergeDeeply.test.js +0 -152
  31. package/src/object/pick.test.js +0 -23
  32. package/src/object/pickBy.test.js +0 -48
  33. package/src/string/camelize.test.js +0 -37
  34. package/src/string/capitalize.test.js +0 -33
  35. package/src/string/decamelize.test.js +0 -83
  36. package/src/string/deindent.test.js +0 -181
  37. package/src/string/format.test.js +0 -196
  38. package/src/string/formatDate.test.js +0 -28
  39. package/src/string/getCommonPrefix.test.js +0 -23
  40. package/src/string/isAbsoluteUrl.test.js +0 -15
  41. package/src/string/isCreditCard.test.js +0 -50
  42. package/src/string/isDomain.test.js +0 -15
  43. package/src/string/isEmail.test.js +0 -37
  44. package/src/string/isHostname.test.js +0 -12
  45. package/src/string/isUrl.test.js +0 -1595
  46. package/src/string/labelize.test.js +0 -39
  47. package/src/timer/debounce.test.js +0 -101
  48. package/src/timer/debounceAsync.test.js +0 -143
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ditojs/utils",
3
- "version": "2.100.0",
3
+ "version": "3.1.0",
4
4
  "type": "module",
5
5
  "description": "Dito.js Utility Functions – Dito.js is a declarative and modern web framework, based on Objection.js, Koa.js and Vue.js",
6
6
  "repository": "https://github.com/ditojs/dito/tree/main/packages/utils",
@@ -17,11 +17,10 @@
17
17
  },
18
18
  "files": [
19
19
  "src/",
20
- "types/"
20
+ "types/",
21
+ "!src/**/*.test.js",
22
+ "!src/**/__snapshots__/"
21
23
  ],
22
- "scripts": {
23
- "types": "tsc --noEmit types/index.d.ts"
24
- },
25
24
  "engines": {
26
25
  "node": ">= 24.0.0"
27
26
  },
@@ -32,11 +31,8 @@
32
31
  "ie >= 11",
33
32
  "ie_mob >= 11"
34
33
  ],
35
- "dependencies": {
36
- "punycode": "^2.3.1"
37
- },
38
34
  "devDependencies": {
39
35
  "typescript": "^6.0.3"
40
36
  },
41
- "gitHead": "c6fdbe03971188779fc9cf5f1b4202e08bc75a65"
37
+ "gitHead": "97314265c5cbac6a248d7350f43fea8ea92b995f"
42
38
  }
@@ -1,7 +1,7 @@
1
1
  export function shuffle(array) {
2
2
  // Do the Fisher-Yates (aka Knuth) Shuffle:
3
3
  const res = array.slice()
4
- for (let i = array.length; i; ) {
4
+ for (let i = array.length; i;) {
5
5
  const r = Math.floor(Math.random() * i)
6
6
  const t = res[--i]
7
7
  res[i] = res[r]
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Formats a date as its local plain date, e.g. `'2026-05-14'`, without the
3
+ * time and time zone of `toISOString()`, which can shift it to another day.
4
+ */
5
+ export function formatPlainDate(date) {
6
+ // Use the date parts, as shifting by `getTimezoneOffset()` misses offsets
7
+ // with seconds, e.g. local mean time before 1894 in Europe/Zurich.
8
+ const pad = (number, length = 2) => String(number).padStart(length, '0')
9
+ return [
10
+ pad(date.getFullYear(), 4),
11
+ pad(date.getMonth() + 1),
12
+ pad(date.getDate())
13
+ ].join('-')
14
+ }
@@ -4,6 +4,7 @@ export * from './decamelize.js'
4
4
  export * from './deindent.js'
5
5
  export * from './escapeRegexp.js'
6
6
  export * from './format.js'
7
+ export * from './formatPlainDate.js'
7
8
  export * from './formatDate.js'
8
9
  export * from './getCommonPrefix.js'
9
10
  export * from './isAbsoluteUrl.js'
@@ -13,3 +14,4 @@ export * from './isEmail.js'
13
14
  export * from './isHostname.js'
14
15
  export * from './isUrl.js'
15
16
  export * from './labelize.js'
17
+ export * from './parsePlainDate.js'
@@ -1,9 +1,16 @@
1
- import punycode from 'punycode/punycode.js'
2
-
3
1
  // Best effort approach, allowing Internationalized domain name (with punycode)
4
2
  const domainRegExp =
5
3
  /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$/i
6
4
 
7
5
  export function isDomain(str) {
8
- return !!(str && domainRegExp.test(punycode.toASCII(str)))
6
+ // Reject characters that `URL` would treat as delimiters or decode, so that
7
+ // only the domain itself is converted to its ASCII (punycode) form.
8
+ if (!str || /[\s/\\?#@:%]/.test(str)) {
9
+ return false
10
+ }
11
+ try {
12
+ return domainRegExp.test(new URL(`http://${str}`).hostname)
13
+ } catch {
14
+ return false
15
+ }
9
16
  }
@@ -0,0 +1,18 @@
1
+ import { formatPlainDate } from './formatPlainDate.js'
2
+
3
+ /**
4
+ * Parses a plain date like `'2026-05-14'` as local midnight, unlike
5
+ * `new Date()`, which parses it as UTC midnight. Returns `null` for anything
6
+ * else, including invalid dates like `'2026-02-30'`.
7
+ */
8
+ export function parsePlainDate(string) {
9
+ if (/^\d{4}-\d{2}-\d{2}$/.test(string)) {
10
+ // Date-time strings without an offset are parsed as local time.
11
+ const date = new Date(`${string}T00:00`)
12
+ // Reject invalid days, which `Date` rolls over to the next month.
13
+ if (formatPlainDate(date) === string) {
14
+ return date
15
+ }
16
+ }
17
+ return null
18
+ }
package/types/index.d.ts CHANGED
@@ -518,6 +518,19 @@ export function format(
518
518
  }
519
519
  }
520
520
  ): string | null | undefined
521
+ /**
522
+ * Formats a date as its local plain date, e.g. `'2026-05-14'`, without the
523
+ * time and time zone of `toISOString()`, which can shift it to another day.
524
+ */
525
+ export function formatPlainDate(date: Date): string
526
+ /**
527
+ * Parses a plain date like `'2026-05-14'` as local midnight, unlike
528
+ * `new Date()`, which parses it as UTC midnight. Returns `null` for anything
529
+ * else, including invalid dates like `'2026-02-30'`.
530
+ */
531
+ export function parsePlainDate(
532
+ string: string | null | undefined
533
+ ): Date | null
521
534
  /**
522
535
  * Formats a date value as a string. If the value is not a Date,
523
536
  * attempts to convert it to a Date first.
@@ -1,88 +0,0 @@
1
- // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
2
-
3
- exports[`index > exports all symbols 1`] = `
4
- {
5
- "asArray": [Function],
6
- "asFunction": [Function],
7
- "asObject": [Function],
8
- "assignDeeply": [Function],
9
- "camelize": [Function],
10
- "capitalize": [Function],
11
- "clone": [Function],
12
- "debounce": [Function],
13
- "debounceAsync": [Function],
14
- "decamelize": [Function],
15
- "defaultFormats": {
16
- "date": {
17
- "day": "numeric",
18
- "month": "long",
19
- "year": "numeric",
20
- },
21
- "number": {
22
- "style": "decimal",
23
- },
24
- "time": {
25
- "hour": "2-digit",
26
- "minute": "2-digit",
27
- "second": "2-digit",
28
- },
29
- },
30
- "deindent": [Function],
31
- "deprecate": [Function],
32
- "equals": [Function],
33
- "escapeHtml": [Function],
34
- "escapeRegexp": [Function],
35
- "flatten": [Function],
36
- "format": [Function],
37
- "formatDate": [Function],
38
- "getCommonOffset": [Function],
39
- "getCommonPrefix": [Function],
40
- "getEntriesAtDataPath": [Function],
41
- "getValueAtDataPath": [Function],
42
- "groupBy": [Function],
43
- "hyphenate": [Function],
44
- "is": [Function],
45
- "isAbsoluteUrl": [Function],
46
- "isArray": [Function],
47
- "isArrayLike": [Function],
48
- "isAsync": [Function],
49
- "isBoolean": [Function],
50
- "isCreditCard": [Function],
51
- "isDate": [Function],
52
- "isDomain": [Function],
53
- "isEmail": [Function],
54
- "isEmpty": [Function],
55
- "isFunction": [Function],
56
- "isHostname": [Function],
57
- "isInteger": [Function],
58
- "isModule": [Function],
59
- "isNumber": [Function],
60
- "isObject": [Function],
61
- "isPlainArray": [Function],
62
- "isPlainObject": [Function],
63
- "isPromise": [Function],
64
- "isRegExp": [Function],
65
- "isString": [Function],
66
- "isUrl": [Function],
67
- "labelize": [Function],
68
- "mapConcurrently": [Function],
69
- "mapKeys": [Function],
70
- "mapSequentially": [Function],
71
- "mapValues": [Function],
72
- "mergeDeeply": [Function],
73
- "mixin": [Function],
74
- "normalizeDataPath": [Function],
75
- "parseDataPath": [Function],
76
- "pick": [Function],
77
- "pickBy": [Function],
78
- "setDataPathEntries": [Function],
79
- "setValueAtDataPath": [Function],
80
- "shuffle": [Function],
81
- "stripHtml": [Function],
82
- "stripTags": [Function],
83
- "toAsync": [Function],
84
- "toCallback": [Function],
85
- "toPromiseCallback": [Function],
86
- "underscore": [Function],
87
- }
88
- `;
@@ -1,29 +0,0 @@
1
- import { flatten } from './flatten.js'
2
-
3
- describe('flatten()', () => {
4
- it('should support flattening of nested arrays', () => {
5
- const array = [1, [2, [3, [4]], 5]]
6
- expect(flatten(array)).toStrictEqual([1, 2, 3, 4, 5])
7
- expect(flatten(array, 1)).toStrictEqual([1, 2, [3, [4]], 5])
8
- expect(flatten(array, 2)).toStrictEqual([1, 2, 3, [4], 5])
9
- })
10
-
11
- it('should work with empty arrays', () => {
12
- const array = [[], [[]], [[], [[[]]]]]
13
-
14
- expect(flatten(array)).toStrictEqual([])
15
- expect(flatten(array, 1)).toStrictEqual([[], [], [[[]]]])
16
- expect(flatten(array, 2)).toStrictEqual([[[]]])
17
- })
18
-
19
- it('should treat sparse arrays as dense', () => {
20
- const array = [[1, 2, 3], Array(3)]
21
- const expected = [1, 2, 3]
22
- expected.push(undefined, undefined, undefined)
23
- expect(flatten(array)).toStrictEqual(expected)
24
- })
25
-
26
- it('should return an empty array for non array-like objects', () => {
27
- expect(flatten({ 0: 'a' })).toStrictEqual([])
28
- })
29
- })
@@ -1,25 +0,0 @@
1
- import { shuffle } from './shuffle.js'
2
-
3
- describe('shuffle()', () => {
4
- const array = [1, 2, 3]
5
-
6
- it('should return a new array', () => {
7
- expect(shuffle(array)).not.toBe(array)
8
- })
9
-
10
- it(`should contain the same elements after a collection is shuffled and sorted again`, () => {
11
- expect(shuffle(array).sort()).toStrictEqual(array)
12
- })
13
-
14
- it('should shuffle small collections', () => {
15
- const results = {}
16
- for (let i = 0; i < 1000; i++) {
17
- const res = shuffle([1, 2])
18
- results[res] = res
19
- }
20
- const sorted = Object.entries(results)
21
- .sort()
22
- .map(([, value]) => value)
23
- expect(sorted).toStrictEqual([[1, 2], [2, 1]])
24
- })
25
- })