@radio-garden/ditojs-utils 2.85.0-0.5067ad799

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 (104) hide show
  1. package/README.md +6 -0
  2. package/package.json +43 -0
  3. package/src/__snapshots__/index.test.js.snap +88 -0
  4. package/src/array/flatten.js +14 -0
  5. package/src/array/flattten.test.js +29 -0
  6. package/src/array/index.js +2 -0
  7. package/src/array/shuffle.js +11 -0
  8. package/src/array/shuffle.test.js +25 -0
  9. package/src/base/base.js +118 -0
  10. package/src/base/base.test.js +590 -0
  11. package/src/base/index.js +1 -0
  12. package/src/class/index.js +1 -0
  13. package/src/class/mixin.js +29 -0
  14. package/src/class/mixin.test.js +70 -0
  15. package/src/dataPath/getEntriesAtDataPath.js +67 -0
  16. package/src/dataPath/getEntriesAtDataPath.test.js +204 -0
  17. package/src/dataPath/getValueAtDataPath.js +45 -0
  18. package/src/dataPath/getValueAtDataPath.test.js +140 -0
  19. package/src/dataPath/index.js +6 -0
  20. package/src/dataPath/normalizeDataPath.js +27 -0
  21. package/src/dataPath/normalizeDataPath.test.js +36 -0
  22. package/src/dataPath/parseDataPath.js +16 -0
  23. package/src/dataPath/parseDataPath.test.js +67 -0
  24. package/src/dataPath/setDataPathEntries.js +8 -0
  25. package/src/dataPath/setDataPathEntries.test.js +36 -0
  26. package/src/dataPath/setValueAtDataPath.js +13 -0
  27. package/src/dataPath/setValueAtDataPath.test.js +34 -0
  28. package/src/function/deprecate.js +9 -0
  29. package/src/function/index.js +4 -0
  30. package/src/function/toAsync.js +9 -0
  31. package/src/function/toAsync.test.js +31 -0
  32. package/src/function/toCallback.js +11 -0
  33. package/src/function/toCallback.test.js +29 -0
  34. package/src/function/toPromiseCallback.js +9 -0
  35. package/src/function/toPromiseCallback.test.js +24 -0
  36. package/src/html/escapeHtml.js +11 -0
  37. package/src/html/escapeHtml.test.js +19 -0
  38. package/src/html/index.js +2 -0
  39. package/src/html/stripHtml.js +23 -0
  40. package/src/html/stripHtml.test.js +37 -0
  41. package/src/index.js +10 -0
  42. package/src/index.test.js +7 -0
  43. package/src/object/asCallback.js +9 -0
  44. package/src/object/clone.js +75 -0
  45. package/src/object/clone.test.js +131 -0
  46. package/src/object/equals.js +36 -0
  47. package/src/object/equals.test.js +269 -0
  48. package/src/object/groupBy.js +15 -0
  49. package/src/object/groupBy.test.js +70 -0
  50. package/src/object/index.js +8 -0
  51. package/src/object/mapKeys.js +7 -0
  52. package/src/object/mapKeys.test.js +16 -0
  53. package/src/object/mapValues.js +9 -0
  54. package/src/object/mapValues.test.js +38 -0
  55. package/src/object/mergeDeeply.js +47 -0
  56. package/src/object/mergeDeeply.test.js +152 -0
  57. package/src/object/pick.js +11 -0
  58. package/src/object/pick.test.js +23 -0
  59. package/src/object/pickBy.js +11 -0
  60. package/src/object/pickBy.test.js +48 -0
  61. package/src/promise/index.js +2 -0
  62. package/src/promise/mapConcurrently.js +33 -0
  63. package/src/promise/mapSequentially.js +9 -0
  64. package/src/string/camelize.js +14 -0
  65. package/src/string/camelize.test.js +37 -0
  66. package/src/string/capitalize.js +7 -0
  67. package/src/string/capitalize.test.js +33 -0
  68. package/src/string/decamelize.js +27 -0
  69. package/src/string/decamelize.test.js +83 -0
  70. package/src/string/deindent.js +69 -0
  71. package/src/string/deindent.test.js +181 -0
  72. package/src/string/escapeRegexp.js +3 -0
  73. package/src/string/format.js +109 -0
  74. package/src/string/format.test.js +196 -0
  75. package/src/string/formatDate.js +13 -0
  76. package/src/string/formatDate.test.js +28 -0
  77. package/src/string/getCommonPrefix.js +35 -0
  78. package/src/string/getCommonPrefix.test.js +23 -0
  79. package/src/string/index.js +15 -0
  80. package/src/string/isAbsoluteUrl.js +7 -0
  81. package/src/string/isAbsoluteUrl.test.js +15 -0
  82. package/src/string/isCreditCard.js +21 -0
  83. package/src/string/isCreditCard.test.js +50 -0
  84. package/src/string/isDomain.js +9 -0
  85. package/src/string/isDomain.test.js +15 -0
  86. package/src/string/isEmail.js +6 -0
  87. package/src/string/isEmail.test.js +37 -0
  88. package/src/string/isHostname.js +8 -0
  89. package/src/string/isHostname.test.js +12 -0
  90. package/src/string/isUrl.js +23 -0
  91. package/src/string/isUrl.test.js +1595 -0
  92. package/src/string/labelize.js +17 -0
  93. package/src/string/labelize.test.js +39 -0
  94. package/src/timer/debounce.js +34 -0
  95. package/src/timer/debounce.test.js +101 -0
  96. package/src/timer/debounceAsync.js +60 -0
  97. package/src/timer/debounceAsync.test.js +143 -0
  98. package/src/timer/index.js +2 -0
  99. package/types/index.d.ts +939 -0
  100. package/types/tests/base.test-d.ts +172 -0
  101. package/types/tests/datapath.test-d.ts +75 -0
  102. package/types/tests/function.test-d.ts +137 -0
  103. package/types/tests/object.test-d.ts +190 -0
  104. package/types/tests/promise.test-d.ts +66 -0
@@ -0,0 +1,34 @@
1
+ import { setValueAtDataPath } from './setValueAtDataPath.js'
2
+
3
+ describe('setValueAtDataPath()', () => {
4
+ const data = {
5
+ object: {
6
+ array: [
7
+ {}
8
+ ],
9
+ number: 10
10
+ }
11
+ }
12
+
13
+ const add = { prop: 'new' }
14
+
15
+ it('should add data at a path to a given object', () => {
16
+ expect(() => setValueAtDataPath(data, 'object.array[0].added', add))
17
+ .not.toThrow()
18
+ expect(data.object.array[0].added).toStrictEqual(add)
19
+ })
20
+
21
+ it('should add data at a path to a given array', () => {
22
+ expect(() => setValueAtDataPath(data, 'object.array[1]', add)).not.toThrow()
23
+ expect(data.object.array[1]).toStrictEqual(add)
24
+ })
25
+
26
+ it('should throw an error with faulty paths', () => {
27
+ expect(() => setValueAtDataPath(data, 'object/unknown/prop', add)).toThrow()
28
+ })
29
+
30
+ it('should throw an error with invalid target', () => {
31
+ expect(() => setValueAtDataPath(data, 'object/number/invalid', add))
32
+ .toThrow('Invalid path: object/number/invalid')
33
+ })
34
+ })
@@ -0,0 +1,9 @@
1
+ const loggedDeprecations = new Set()
2
+
3
+ export function deprecate(message) {
4
+ // Only log deprecation messages once.
5
+ if (!loggedDeprecations.has(message)) {
6
+ loggedDeprecations.add(message)
7
+ console.warn(message)
8
+ }
9
+ }
@@ -0,0 +1,4 @@
1
+ export * from './deprecate.js'
2
+ export * from './toAsync.js'
3
+ export * from './toCallback.js'
4
+ export * from './toPromiseCallback.js'
@@ -0,0 +1,9 @@
1
+ import { toPromiseCallback } from './toPromiseCallback.js'
2
+
3
+ export function toAsync(callbackFunction) {
4
+ return (...args) => {
5
+ return new Promise((resolve, reject) => {
6
+ callbackFunction(...args, toPromiseCallback(resolve, reject))
7
+ })
8
+ }
9
+ }
@@ -0,0 +1,31 @@
1
+ import { toAsync } from './toAsync.js'
2
+
3
+ describe('toAsync()', () => {
4
+ it('should convert callback functions to async', async () => {
5
+ expect.assertions(1)
6
+ const asyncFunc = toAsync(function (toResolve, callback) {
7
+ process.nextTick(() => {
8
+ callback(null, toResolve)
9
+ })
10
+ })
11
+ const expected = 10
12
+ const actual = await asyncFunc(expected)
13
+ expect(actual).toBe(expected)
14
+ })
15
+
16
+ it('should convert callback errors to exceptions', async () => {
17
+ expect.assertions(1)
18
+ const error = new Error('This error is intentional')
19
+ const throwError = toAsync(function (toReject, callback) {
20
+ process.nextTick(() => {
21
+ callback(toReject)
22
+ })
23
+ })
24
+
25
+ try {
26
+ await throwError(error)
27
+ } catch (err) {
28
+ expect(err).toBe(error)
29
+ }
30
+ })
31
+ })
@@ -0,0 +1,11 @@
1
+ export function toCallback(asyncFunction) {
2
+ return async (...args) => {
3
+ // The last argument is the callback function
4
+ const done = args.pop()
5
+ try {
6
+ done(null, await asyncFunction(...args))
7
+ } catch (err) {
8
+ done(err)
9
+ }
10
+ }
11
+ }
@@ -0,0 +1,29 @@
1
+ import { toCallback } from './toCallback.js'
2
+
3
+ describe('toCallback()', () => {
4
+ it('should convert async functions to callbacks', async () => {
5
+ expect.assertions(2)
6
+ const callback = toCallback(async result => {
7
+ await Promise.resolve()
8
+ return result
9
+ })
10
+ const expected = 10
11
+ callback(expected, (err, actual) => {
12
+ expect(err).toBeNull()
13
+ expect(actual).toBe(expected)
14
+ })
15
+ })
16
+
17
+ it('should convert async exceptions to callback errors', async () => {
18
+ expect.assertions(2)
19
+ const error = new Error('This error is intentional')
20
+ const callback = toCallback(async error => {
21
+ await Promise.resolve()
22
+ throw error
23
+ })
24
+ callback(error, (err, actual) => {
25
+ expect(err).toBe(error)
26
+ expect(actual).toBe(undefined)
27
+ })
28
+ })
29
+ })
@@ -0,0 +1,9 @@
1
+ export function toPromiseCallback(resolve, reject) {
2
+ return (err, res) => {
3
+ if (err) {
4
+ reject(err)
5
+ } else {
6
+ resolve(res)
7
+ }
8
+ }
9
+ }
@@ -0,0 +1,24 @@
1
+ import { toPromiseCallback } from './toPromiseCallback.js'
2
+ import { vi } from 'vitest'
3
+
4
+ describe('toPromiseCallback()', () => {
5
+ it('should call reject() when called with an error', () => {
6
+ const error = new Error('My Error')
7
+ const resolve = vi.fn()
8
+ const reject = vi.fn()
9
+ const callback = toPromiseCallback(resolve, reject)
10
+ callback(error)
11
+ expect(resolve).not.toBeCalled()
12
+ expect(reject).toBeCalledWith(error)
13
+ })
14
+
15
+ it('should call resolve() when called with a result', () => {
16
+ const result = 42
17
+ const resolve = vi.fn()
18
+ const reject = vi.fn()
19
+ const callback = toPromiseCallback(resolve, reject)
20
+ callback(null, result)
21
+ expect(reject).not.toBeCalled()
22
+ expect(resolve).toBeCalledWith(result)
23
+ })
24
+ })
@@ -0,0 +1,11 @@
1
+ export function escapeHtml(html) {
2
+ return html != null
3
+ ? html
4
+ .toString()
5
+ .replace(
6
+ /["&<>]/g,
7
+ chr =>
8
+ ({ '"': '&quot;', '&': '&amp;', '<': '&lt;', '>': '&gt;' })[chr]
9
+ )
10
+ : ''
11
+ }
@@ -0,0 +1,19 @@
1
+ import { escapeHtml } from './escapeHtml.js'
2
+
3
+ describe('escapeHtml()', () => {
4
+ it('should escape quotes, ampersands, and smaller/greater than signs', () => {
5
+ expect(escapeHtml('<div id="me, myself & i"/>'))
6
+ .toBe('&lt;div id=&quot;me, myself &amp; i&quot;/&gt;')
7
+ })
8
+
9
+ it('should return an empty string if nothing can be processed', () => {
10
+ expect(escapeHtml()).toBe('')
11
+ expect(escapeHtml(null)).toBe('')
12
+ expect(escapeHtml('')).toBe('')
13
+ })
14
+
15
+ it('should handle falsy values correctly', () => {
16
+ expect(escapeHtml(0)).toBe('0')
17
+ expect(escapeHtml(false)).toBe('false')
18
+ })
19
+ })
@@ -0,0 +1,2 @@
1
+ export * from './escapeHtml.js'
2
+ export * from './stripHtml.js'
@@ -0,0 +1,23 @@
1
+ import { deprecate } from '../function/deprecate.js'
2
+
3
+ export function stripHtml(html) {
4
+ return html != null
5
+ ? html
6
+ .toString()
7
+ .replace(/<br\s*\/?>/gi, '\n')
8
+ .replace(/<\/p>/gi, '\n')
9
+ .replace(/<[^>]+>/g, '')
10
+ .trim()
11
+ : ''
12
+ }
13
+
14
+ /**
15
+ * @deprecated Use stripHtml() instead
16
+ */
17
+ export function stripTags(html) {
18
+ deprecate(
19
+ 'The `stripTags` function is deprecated in favour of `stripHtml`. ' +
20
+ 'Update your code to use `stripHtml` instead.'
21
+ )
22
+ return stripHtml(html)
23
+ }
@@ -0,0 +1,37 @@
1
+ import { stripHtml } from './stripHtml.js'
2
+
3
+ describe('stripHtml()', () => {
4
+ it('should remove html tags from strings', () => {
5
+ expect(stripHtml('<p><b>this</b> is a marked-up string</p>'))
6
+ .toBe('this is a marked-up string')
7
+ })
8
+
9
+ it('should return an empty string if nothing can be processed', () => {
10
+ expect(stripHtml()).toBe('')
11
+ expect(stripHtml(null)).toBe('')
12
+ expect(stripHtml('')).toBe('')
13
+ })
14
+
15
+ it('should handle falsy values correctly', () => {
16
+ expect(stripHtml(0)).toBe('0')
17
+ expect(stripHtml(false)).toBe('false')
18
+ })
19
+
20
+ it('should convert <br> tags to line-breaks', () => {
21
+ expect(stripHtml('hello<br>world')).toBe('hello\nworld')
22
+ expect(stripHtml('hello<br/>world')).toBe('hello\nworld')
23
+ expect(stripHtml('hello<br />world')).toBe('hello\nworld')
24
+ })
25
+
26
+ it('should convert <p> tags to line-breaks', () => {
27
+ expect(stripHtml('<p>First paragraph</p><p>Second paragraph</p>'))
28
+ .toBe('First paragraph\nSecond paragraph')
29
+ expect(stripHtml('<p>Only paragraph</p>'))
30
+ .toBe('Only paragraph')
31
+ })
32
+
33
+ it('should trim leading and trailing whitespace', () => {
34
+ expect(stripHtml(' <p>text</p> ')).toBe('text')
35
+ expect(stripHtml('<p> text </p>')).toBe('text')
36
+ })
37
+ })
package/src/index.js ADDED
@@ -0,0 +1,10 @@
1
+ export * from './base/index.js'
2
+ export * from './class/index.js'
3
+ export * from './array/index.js'
4
+ export * from './object/index.js'
5
+ export * from './string/index.js'
6
+ export * from './timer/index.js'
7
+ export * from './promise/index.js'
8
+ export * from './function/index.js'
9
+ export * from './dataPath/index.js'
10
+ export * from './html/index.js'
@@ -0,0 +1,7 @@
1
+ import * as index from './index.js'
2
+
3
+ describe('index', () => {
4
+ it('exports all symbols', () => {
5
+ expect(index).toMatchSnapshot()
6
+ })
7
+ })
@@ -0,0 +1,9 @@
1
+ import { isString } from '../base/index.js'
2
+
3
+ export function asCallback(callback) {
4
+ if (isString(callback)) {
5
+ const key = callback
6
+ return value => value[key]
7
+ }
8
+ return callback
9
+ }
@@ -0,0 +1,75 @@
1
+ import {
2
+ isArray,
3
+ isObject,
4
+ isDate,
5
+ isRegExp,
6
+ isFunction,
7
+ isPromise
8
+ } from '../base/index.js'
9
+ import { pick } from './pick.js'
10
+
11
+ export function clone(value, options) {
12
+ const {
13
+ shallow = false,
14
+ enumerable = true,
15
+ descriptors = !enumerable,
16
+ transferables = null,
17
+ processValue = null
18
+ } = options ?? {}
19
+
20
+ const clones = new Map()
21
+
22
+ const storeClone = (value, copy) => {
23
+ clones.set(value, copy)
24
+ return copy
25
+ }
26
+
27
+ const handleValue = value =>
28
+ shallow || transferables?.includes(value)
29
+ ? value
30
+ : clones.has(value)
31
+ ? clones.get(value)
32
+ : cloneValue(value)
33
+
34
+ const cloneValue = value => {
35
+ let copy = value
36
+ if (isDate(value)) {
37
+ copy = storeClone(value, new value.constructor(+value))
38
+ } else if (isRegExp(value)) {
39
+ copy = storeClone(value, new value.constructor(value))
40
+ } else if (isArray(value)) {
41
+ copy = storeClone(value, new value.constructor(value.length))
42
+ for (let i = 0, l = value.length; i < l; i++) {
43
+ copy[i] = handleValue(value[i])
44
+ }
45
+ } else if (isObject(value)) {
46
+ // Rely on arg.clone() if it exists and assume it creates an actual clone.
47
+ if (isFunction(value.clone)) {
48
+ copy = storeClone(value, value.clone(options))
49
+ } else if (isPromise(value)) {
50
+ // https://stackoverflow.com/questions/37063293/can-i-clone-a-promise
51
+ copy = storeClone(value, value.then())
52
+ } else {
53
+ // Prevent calling the actual constructor since it is not guaranteed to
54
+ // work as intended here, and only clone non-inherited own properties.
55
+ copy = storeClone(value, Object.create(Object.getPrototypeOf(value)))
56
+ clones.set(value, copy)
57
+ const keys = enumerable ? Object.keys(value) : Reflect.ownKeys(value)
58
+ for (const key of keys) {
59
+ if (descriptors) {
60
+ const desc = Reflect.getOwnPropertyDescriptor(value, key)
61
+ if (desc.value != null) {
62
+ desc.value = handleValue(desc.value)
63
+ }
64
+ Reflect.defineProperty(copy, key, desc)
65
+ } else {
66
+ copy[key] = handleValue(value[key])
67
+ }
68
+ }
69
+ }
70
+ }
71
+ return pick(processValue?.(copy), copy)
72
+ }
73
+
74
+ return cloneValue(value)
75
+ }
@@ -0,0 +1,131 @@
1
+ import { clone } from './clone.js'
2
+ import { vi } from 'vitest'
3
+
4
+ describe('clone()', () => {
5
+ it('should clone objects', () => {
6
+ const object = { a: 1, b: 2 }
7
+ const copy = clone(object)
8
+ expect(copy).toEqual(object)
9
+ expect(copy).not.toBe(object)
10
+ })
11
+
12
+ it('should clone arrays', () => {
13
+ const array = [1, 2, 3]
14
+ const copy = clone(array)
15
+ expect(copy).toStrictEqual(array)
16
+ expect(copy).not.toBe(array)
17
+ })
18
+
19
+ it('should clone dates', () => {
20
+ const date = new Date(2012, 5, 9)
21
+ const copy = clone(date)
22
+ expect(copy).toStrictEqual(date)
23
+ expect(copy).not.toBe(date)
24
+ })
25
+
26
+ it('should clone regular expressions', () => {
27
+ const regexp = /regexp/gi
28
+ const copy = clone(regexp)
29
+ expect(copy).toStrictEqual(regexp)
30
+ expect(copy).not.toBe(regexp)
31
+ })
32
+
33
+ it('should return functions unmodified', () => {
34
+ const func = () => {}
35
+ expect(clone(func)).toBe(func)
36
+ })
37
+
38
+ it('should clone nested objects and arrays', () => {
39
+ const object = { a: [1, 2, 3], b: { c: 4, d: 5 } }
40
+ const copy = clone(object)
41
+ expect(copy).toStrictEqual(object)
42
+ expect(copy).not.toBe(object)
43
+ expect(copy.a).not.toBe(object.a)
44
+ expect(copy.b).not.toBe(object.b)
45
+ })
46
+
47
+ it('should support shallow-cloning', () => {
48
+ const object = { a: [1, 2, 3] }
49
+ const copy = clone(object, { shallow: true })
50
+ expect(object.a).toBe(copy.a)
51
+ })
52
+
53
+ it('should use clone() methods if available', () => {
54
+ const object = {
55
+ a: 1,
56
+ clone: vi.fn(() => ({ b: 2 }))
57
+ }
58
+ const copy = clone(object)
59
+ expect(object.clone).toBeCalledTimes(1)
60
+ expect(copy).toStrictEqual({ b: 2 })
61
+ })
62
+
63
+ it('should preserve identity', async () => {
64
+ const object = { a: { b: 1 } }
65
+ object.c = object.a
66
+ const copy = clone(object)
67
+ expect(copy).toEqual(object)
68
+ expect(copy.c).toBe(copy.a)
69
+ })
70
+
71
+ it('should handle circular references', async () => {
72
+ const object = { a: { b: 1 } }
73
+ object.c = object
74
+ const copy = clone(object)
75
+ expect(copy).toEqual(object)
76
+ expect(copy.c).toBe(copy)
77
+ })
78
+
79
+ it('should handle non-enumerable properties', async () => {
80
+ const object = { a: 1 }
81
+ Object.defineProperty(object, 'b', { value: 2, enumerable: false })
82
+ const copy = clone(object, { enumerable: false })
83
+ expect(copy.a).toEqual(1)
84
+ expect(copy.b).toEqual(2)
85
+ })
86
+
87
+ it('should transform cloned values by `processValue`', () => {
88
+ const object = {
89
+ a: { b: 1, c: 2 },
90
+ d: { e: 3, f: 4 }
91
+ }
92
+ const copy = clone(object, {
93
+ processValue: value => {
94
+ if (typeof value === 'object') {
95
+ value.g = 5
96
+ }
97
+ }
98
+ })
99
+ const expected = {
100
+ a: { b: 1, c: 2, g: 5 },
101
+ d: { e: 3, f: 4, g: 5 },
102
+ g: 5
103
+ }
104
+ expect(copy).toStrictEqual(expected)
105
+ })
106
+
107
+ it('should call `processValue` after cloning all children', () => {
108
+ const array = [
109
+ { a: 1, b: 2 },
110
+ { a: 3, b: 4 }
111
+ ]
112
+ const copy = clone(array, {
113
+ processValue: value => {
114
+ if (typeof value === 'object') {
115
+ delete value.b
116
+ }
117
+ }
118
+ })
119
+ const expected = [
120
+ { a: 1 },
121
+ { a: 3 }
122
+ ]
123
+ expect(copy).toStrictEqual(expected)
124
+ })
125
+
126
+ it('should handle promises', async () => {
127
+ const promise = (async () => 1)()
128
+ const result = clone(promise)
129
+ expect(await result).toStrictEqual(1)
130
+ })
131
+ })
@@ -0,0 +1,36 @@
1
+ import { is, isArray, isObject } from '../base/index.js'
2
+
3
+ export function equals(arg1, arg2) {
4
+ if (arg1 === arg2) {
5
+ return true
6
+ }
7
+ if (arg1 != null && arg2 != null) {
8
+ arg1 = arg1.valueOf()
9
+ arg2 = arg2.valueOf()
10
+ if (is(arg1, arg2)) {
11
+ return true
12
+ }
13
+ if (isArray(arg1) && isArray(arg2)) {
14
+ let { length } = arg1
15
+ if (length === arg2.length) {
16
+ while (length--) {
17
+ if (!equals(arg1[length], arg2[length])) {
18
+ return false
19
+ }
20
+ }
21
+ return true
22
+ }
23
+ } else if (isObject(arg1) && isObject(arg2)) {
24
+ const keys = Object.keys(arg1)
25
+ if (keys.length === Object.keys(arg2).length) {
26
+ for (const key of keys) {
27
+ if (!(arg2.hasOwnProperty(key) && equals(arg1[key], arg2[key]))) {
28
+ return false
29
+ }
30
+ }
31
+ return true
32
+ }
33
+ }
34
+ }
35
+ return false
36
+ }