@splendidlabz/utils 1.3.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 (93) hide show
  1. package/.eslintrc.cjs +3 -0
  2. package/.turbo/turbo-lint.log +10 -0
  3. package/.turbo/turbo-test.log +10 -0
  4. package/CHANGELOG.md +37 -0
  5. package/actions/index.js +5 -0
  6. package/actions/masonry.js +38 -0
  7. package/actions/mutation-observer.js +43 -0
  8. package/actions/prefer-horizontal-scroll.js +125 -0
  9. package/actions/resize-observer.js +38 -0
  10. package/actions/sticky.js +88 -0
  11. package/dom/bounding-box.js +54 -0
  12. package/dom/clipboard.js +14 -0
  13. package/dom/cookie.js +31 -0
  14. package/dom/css-vars.js +11 -0
  15. package/dom/events.js +76 -0
  16. package/dom/events.test.js +99 -0
  17. package/dom/focusable.js +56 -0
  18. package/dom/font-size.js +24 -0
  19. package/dom/get-element.js +69 -0
  20. package/dom/hash.js +9 -0
  21. package/dom/index.js +18 -0
  22. package/dom/keyboard.js +39 -0
  23. package/dom/local-store.js +29 -0
  24. package/dom/media.js +27 -0
  25. package/dom/pkce.js +34 -0
  26. package/dom/query-params.js +14 -0
  27. package/dom/random-string.js +18 -0
  28. package/dom/session-store.js +29 -0
  29. package/dom/trap-focus.js +55 -0
  30. package/dom/ui/aria-current.js +20 -0
  31. package/dom/ui/inconsistent-button-fix.js +8 -0
  32. package/dom/ui/index.js +4 -0
  33. package/dom/ui/scroll-container.js +25 -0
  34. package/dom/ui/traverse-and-scramble.js +40 -0
  35. package/lib/arrays/index.js +62 -0
  36. package/lib/auth/index.js +1 -0
  37. package/lib/auth/route-manager.js +44 -0
  38. package/lib/checks.js +11 -0
  39. package/lib/date/days.js +9 -0
  40. package/lib/date/index.js +2 -0
  41. package/lib/date/months.js +74 -0
  42. package/lib/form/form-data.js +73 -0
  43. package/lib/form/index.js +2 -0
  44. package/lib/form/sanitize.js +47 -0
  45. package/lib/functions/debounce.js +18 -0
  46. package/lib/functions/env.js +5 -0
  47. package/lib/functions/functional.js +31 -0
  48. package/lib/functions/index.js +5 -0
  49. package/lib/functions/throttle.js +11 -0
  50. package/lib/functions/timeout.js +15 -0
  51. package/lib/index.js +12 -0
  52. package/lib/index.test.js +9 -0
  53. package/lib/numbers/index.js +10 -0
  54. package/lib/objects/camelcase-keys.js +17 -0
  55. package/lib/objects/camelcase-keys.test.js +72 -0
  56. package/lib/objects/empty.js +10 -0
  57. package/lib/objects/extend.js +16 -0
  58. package/lib/objects/extend.test.js +35 -0
  59. package/lib/objects/flatten.js +19 -0
  60. package/lib/objects/index.js +12 -0
  61. package/lib/objects/json.js +7 -0
  62. package/lib/objects/loop.js +11 -0
  63. package/lib/objects/loop.test.js +31 -0
  64. package/lib/objects/mix/mix.js +100 -0
  65. package/lib/objects/mix/mix.md +78 -0
  66. package/lib/objects/mix/mix.test.js +324 -0
  67. package/lib/objects/nested-property.js +36 -0
  68. package/lib/objects/normalize-object.js +10 -0
  69. package/lib/objects/omit-empty.js +23 -0
  70. package/lib/objects/omit-empty.test.js +85 -0
  71. package/lib/objects/size.js +41 -0
  72. package/lib/objects/split.js +19 -0
  73. package/lib/promises/index.js +1 -0
  74. package/lib/promises/reject.js +17 -0
  75. package/lib/strings/convert-case/convert-case.js +86 -0
  76. package/lib/strings/convert-case/convert-case.md +44 -0
  77. package/lib/strings/convert-case/convert-case.test.js +50 -0
  78. package/lib/strings/index.js +4 -0
  79. package/lib/strings/markdown.js +39 -0
  80. package/lib/strings/pluralize.js +2 -0
  81. package/lib/strings/query-string.js +18 -0
  82. package/lib/style/index.js +11 -0
  83. package/lib/symbols/index.js +1 -0
  84. package/lib/symbols/symbols.js +9 -0
  85. package/node/common.js +5 -0
  86. package/node/dirname.js +17 -0
  87. package/node/file-cache.js +135 -0
  88. package/node/file.js +13 -0
  89. package/node/hash.js +5 -0
  90. package/node/index.js +6 -0
  91. package/node/pkce.js +33 -0
  92. package/node/random-string.js +10 -0
  93. package/package.json +35 -0
@@ -0,0 +1,86 @@
1
+ // ========================
2
+ // Case Conversion Utilities
3
+ // Note: All case conversion functions require `toKebab` to work.
4
+ // ------------------------
5
+ export function toKebab(string) {
6
+ if (!string) return ''
7
+ return string
8
+ .split('')
9
+ .map((letter, index) => {
10
+ const previousLetter = string[index - 1] || ''
11
+ const currentLetter = letter
12
+
13
+ if (isDigit(currentLetter) && !isDigit(previousLetter)) {
14
+ return `-${currentLetter}`
15
+ }
16
+
17
+ if (!isCaps(currentLetter)) return currentLetter
18
+
19
+ if (previousLetter === '') {
20
+ return `${currentLetter.toLowerCase()}`
21
+ }
22
+
23
+ if (isCaps(previousLetter)) {
24
+ return `${currentLetter.toLowerCase()}`
25
+ }
26
+
27
+ return `-${currentLetter.toLowerCase()}`
28
+ })
29
+ .join('')
30
+ .trim()
31
+ .replace(/[-_\s]+/g, '-')
32
+ }
33
+
34
+ export function toCamel(string) {
35
+ return toKebab(string)
36
+ .split('-')
37
+ .map((word, index) => {
38
+ if (index === 0) return word
39
+ return word.slice(0, 1).toUpperCase() + word.slice(1).toLowerCase()
40
+ })
41
+ .join('')
42
+ }
43
+
44
+ export function toPascal(string) {
45
+ const camel = toCamel(string)
46
+ return camel.slice(0, 1).toUpperCase() + camel.slice(1)
47
+ }
48
+
49
+ export function toTitle(string) {
50
+ return toKebab(string)
51
+ .split('-')
52
+ .map(word => {
53
+ return word.slice(0, 1).toUpperCase() + word.slice(1)
54
+ })
55
+ .join(' ')
56
+ }
57
+
58
+ export function toSlug(string) {
59
+ return toKebab(string)
60
+ }
61
+
62
+ export function toSentence(string) {
63
+ const interim = toKebab(string).replace(/-/g, ' ')
64
+ return interim.slice(0, 1).toUpperCase() + interim.slice(1)
65
+ }
66
+
67
+ export function toUpper(string) {
68
+ const interim = toKebab(string).replace(/-/g, ' ')
69
+ return interim.toUpperCase()
70
+ }
71
+
72
+ export function toLower(string) {
73
+ const interim = toKebab(string).replace(/-/g, ' ')
74
+ return interim.toLowerCase()
75
+ }
76
+
77
+ // Checks whether character is Uppercase.
78
+ // Crude version. Checks only A-Z.
79
+ function isCaps(char) {
80
+ return /\p{Lu}/u.test(char)
81
+ }
82
+
83
+ // Checks whether character is digit.
84
+ function isDigit(char) {
85
+ return /[0-9]/.test(char)
86
+ }
@@ -0,0 +1,44 @@
1
+ # Case Conversion Utilities
2
+
3
+ This file contains utilities to convert any string into the following cases:
4
+
5
+ 1. `kebab-case`
6
+ 2. `camelCase`
7
+ 3. `Title Case`
8
+ 4. `Sentence case`
9
+
10
+ [See my process for creating these case-conversion utilties](https://zellwk.com/blog/case-conversion)
11
+
12
+ ## Examples
13
+
14
+ ```javascript
15
+ toKebab('caseWithSomeWords') // case-with-some-words
16
+ toKebab('CaseWithSomeWords') // case-with-some-words
17
+ toKebab('case_with_some_words') // case-with-some-words
18
+ toKebab('Case with some words') // case-with-some-words
19
+ toKebab('Case With Some Words') // case-with-some-words
20
+ toKebab('Case_with SomeWords') // case-with-some-words
21
+ ```
22
+
23
+ ## Installation
24
+
25
+ Manual installation:
26
+
27
+ - Copy and paste the entire file into your javascript file. 😉
28
+ - If you use ES5 Modules, you can import them into your file
29
+
30
+ ```js
31
+ import { toKebab } from "./convert-case.js"
32
+ toKebab('someString')
33
+ ```
34
+
35
+ NPM:
36
+
37
+ - Run `npm install @zellwk/javascript`
38
+ - Import `convert-case.js`
39
+
40
+ ```js
41
+ import { toKebab } from "@zellwk/javascript/convert-case"
42
+ toKebab('someString')
43
+ ```
44
+
@@ -0,0 +1,50 @@
1
+ /* eslint-env jest */
2
+ import { describe, expect, it } from 'vitest'
3
+ import { toCamel, toKebab, toSentence, toTitle } from './convert-case'
4
+
5
+ const cases = {
6
+ kebab: 'case-with-long-name100',
7
+ snake: 'case_with_long_name100',
8
+ camel: 'caseWithLongName100',
9
+ pascal: 'CaseWithLongName100',
10
+ sentence: 'Case with long name100',
11
+ title: 'Case With Long Name100',
12
+ constant: 'CASE_WITH_LONG_NAME100',
13
+ mixCases: 'case_WITH-Long name100',
14
+ }
15
+
16
+ describe('To Kebab', _ => {
17
+ Object.entries(cases).forEach(c => {
18
+ it(`From ${c[0]}`, () => {
19
+ const result = toKebab(c[1])
20
+ expect(result, c[0]).toBe('case-with-long-name-100')
21
+ })
22
+ })
23
+ })
24
+
25
+ describe('To Camel', _ => {
26
+ Object.entries(cases).forEach(c => {
27
+ it(`From ${c[0]}`, () => {
28
+ const result = toCamel(c[1])
29
+ expect(result, c[0]).toBe('caseWithLongName100')
30
+ })
31
+ })
32
+ })
33
+
34
+ describe('To Sentence', _ => {
35
+ Object.entries(cases).forEach(c => {
36
+ it(`From ${c[0]}`, () => {
37
+ const result = toSentence(c[1])
38
+ expect(result, c[0]).toBe('Case with long name 100')
39
+ })
40
+ })
41
+ })
42
+
43
+ describe('To Title', _ => {
44
+ Object.entries(cases).forEach(c => {
45
+ it(`From ${c[0]}`, () => {
46
+ const result = toTitle(c[1])
47
+ expect(result, c[0]).toBe('Case With Long Name 100')
48
+ })
49
+ })
50
+ })
@@ -0,0 +1,4 @@
1
+ export * from './convert-case/convert-case.js'
2
+ export * from './markdown.js'
3
+ export * from './pluralize.js'
4
+ export * from './query-string.js'
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Removes leading whitespaces from a Markdown content so they can be rendered appropriately.
3
+ * @param {string} content - The content to treat
4
+ * @param {Boolean} options.inline - Whether content is inline, not block
5
+ * @returns
6
+ */
7
+ export function treatMarkdownWhitespace(
8
+ content,
9
+ { inline, isSlot = false } = {},
10
+ ) {
11
+ const lines = content.split('\n')
12
+
13
+ // Check for inline content
14
+ if (lines[0] !== '' && inline) return { content, inline: true }
15
+
16
+ // If it's not inline content, we need to strip indentation
17
+ // For this, we need to check for the first new line with leading spaces
18
+ const firstLineWithLeadingSpacesIndex = lines.findIndex(line =>
19
+ line.match(/^\s+/),
20
+ )
21
+
22
+ const firstLineWithLeadingSpaces = lines[firstLineWithLeadingSpacesIndex]
23
+
24
+ const indentation =
25
+ firstLineWithLeadingSpaces?.match(/^\s+/)?.[0]?.length ?? 0
26
+
27
+ // Once the indentation is found, we strip the indentation from each line. This treats the Markdown whitespace and prevents it from being rendered as code blocks.
28
+ const retContent = lines
29
+ .map((line, index) => {
30
+ if (index < firstLineWithLeadingSpacesIndex) return line + '\n'
31
+ return line.slice(indentation) + '\n'
32
+ })
33
+ .join('')
34
+
35
+ return {
36
+ content: retContent,
37
+ inline,
38
+ }
39
+ }
@@ -0,0 +1,2 @@
1
+ import pluralize from 'pluralize'
2
+ export { pluralize }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Converts object into a query string
3
+ * @param {Object} object
4
+ * @returns
5
+ */
6
+ export function toQueryString(object) {
7
+ const searchParams = new URLSearchParams(object)
8
+ return searchParams.toString()
9
+ }
10
+
11
+ export function plural(count, noun, suffix = 's') {
12
+ console.warn('plural is deprecated. Use pluralize instead')
13
+ return `${count} ${noun}${count !== 1 ? suffix : ''}`
14
+ }
15
+
16
+ export function stripNumbers(string) {
17
+ return string.replace(/\d*/, '')
18
+ }
@@ -0,0 +1,11 @@
1
+ import { toKebab } from '../strings/convert-case/convert-case.js'
2
+
3
+ export function toStyleString(style) {
4
+ return Object.entries(style)
5
+ .map(([k, v]) => {
6
+ if (k.startsWith('--')) return `${k}:${v}`
7
+ else return `${toKebab(k)}:${v}`
8
+ })
9
+ .join('; ')
10
+ .concat(';')
11
+ }
@@ -0,0 +1 @@
1
+ export * from './symbols.js'
@@ -0,0 +1,9 @@
1
+ export function getSymbol(object, description) {
2
+ return Object.getOwnPropertySymbols(object).find(
3
+ s => s.description === description,
4
+ )
5
+ }
6
+
7
+ export function getSymbolValue(object, description) {
8
+ return object[getSymbol(object, description)]
9
+ }
package/node/common.js ADDED
@@ -0,0 +1,5 @@
1
+ import crypto from 'crypto'
2
+
3
+ export function uuid() {
4
+ return crypto.randomUUID()
5
+ }
@@ -0,0 +1,17 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import path from 'path'
3
+
4
+ /**
5
+ * Returns __dirname
6
+ * @param url — import.meta.url
7
+ * @returns
8
+ */
9
+ export function __dirname(url) {
10
+ if (!url) throw Error("Please provide 'import.meta.url' to dirname")
11
+ return path.dirname(fileURLToPath(url))
12
+ }
13
+
14
+ // Just an alias
15
+ export function dirname(url) {
16
+ return __dirname(url)
17
+ }
@@ -0,0 +1,135 @@
1
+ import fs from 'fs/promises'
2
+ import glob from 'glob-promise'
3
+ import path from 'node:path'
4
+ import { sort } from '../lib/arrays/index.js'
5
+
6
+ // Newer and nicer API
7
+ export async function createCache(dir) {
8
+ const absDirpath = path.join(process.cwd(), dir)
9
+ await fs.mkdir(absDirpath, { recursive: true })
10
+
11
+ return {
12
+ async cacheAt(file) {
13
+ if (file) {
14
+ // Return the cached file's modified timestamp
15
+ const absFilepath = path.join(absDirpath, file)
16
+ try {
17
+ const stat = await fs.stat(absFilepath)
18
+ return stat.mtimeMs
19
+ } catch (error) {
20
+ return 0
21
+ }
22
+ } else {
23
+ // Return the last modified timestamp of the cache directory
24
+ return getLastModifiedTime(dir + '/**/*')
25
+ }
26
+ },
27
+
28
+ getAbsPath(file) {
29
+ return path.join(absDirpath, file)
30
+ },
31
+
32
+ async hasFile(file) {
33
+ const absFilepath = path.join(absDirpath, file)
34
+ try {
35
+ await fs.stat(absFilepath)
36
+ return true
37
+ } catch (e) {
38
+ return false
39
+ }
40
+ },
41
+
42
+ async loadFile(file) {
43
+ try {
44
+ const absFilepath = path.join(absDirpath, file)
45
+ return fs.readFile(absFilepath)
46
+ } catch (error) {
47
+ console.warn(`File not found: ${file}`)
48
+ }
49
+ },
50
+
51
+ async saveFile(file, data) {
52
+ const absfilepath = path.join(absDirpath, file)
53
+ await fs.writeFile(absfilepath, data)
54
+ },
55
+
56
+ async loadJSON(file) {
57
+ const data = await this.loadFile(file)
58
+ try {
59
+ return JSON.parse(data)
60
+ } catch (err) {
61
+ console.warn(`Error parsing JSON: ${file}`)
62
+ return {}
63
+ }
64
+ },
65
+
66
+ async saveJSON(file, data) {
67
+ await this.saveFile(file, JSON.stringify(data))
68
+ },
69
+ }
70
+ }
71
+
72
+ // Legacy, not so nice API
73
+ export async function fileCache({ dir, file }) {
74
+ // Makes the path if it doesn't exist
75
+ await fs.mkdir(path.join(process.cwd(), dir), { recursive: true })
76
+ const cachePath = path.join(process.cwd(), dir, `${file}`)
77
+
78
+ let modifiedAt = 0
79
+
80
+ try {
81
+ const stat = await fs.stat(cachePath)
82
+ modifiedAt = stat.mtimeMs
83
+ } catch (error) {} // Silent error
84
+
85
+ return {
86
+ modifiedAt,
87
+
88
+ async load() {
89
+ return fs.readFile(cachePath)
90
+ },
91
+
92
+ async save(data) {
93
+ await fs.writeFile(cachePath, data)
94
+ },
95
+
96
+ async loadJSON() {
97
+ return JSON.parse(await fs.readFile(cachePath))
98
+ },
99
+ async saveJSON(data) {
100
+ await fs.writeFile(cachePath, JSON.stringify(data))
101
+ },
102
+ }
103
+ }
104
+
105
+ export async function getLastModifiedTime(globPath) {
106
+ let files
107
+
108
+ if (typeof globPath === 'string') {
109
+ files = await glob(removeFirstSlash(globPath))
110
+ }
111
+
112
+ if (Array.isArray(globPath)) {
113
+ const a = await Promise.all(
114
+ globPath.map(path => glob(removeFirstSlash(path))),
115
+ )
116
+ files = a.flat()
117
+ }
118
+
119
+ files = await Promise.all(files.map(fs.stat))
120
+ files = files.map(file => file.mtimeMs)
121
+
122
+ const sorted = sort(files, { sortBy: 'mtimeMs', order: 'desc' })
123
+ return sorted[0] || 0
124
+ }
125
+
126
+ export async function getLatestModifiedTime(globPath) {
127
+ console.warn(
128
+ '`getLatestModifiedTime` has been renamed to `getLastModifiedTime` and will be deprecated in future versions. Please use `getLastModifiedTime` instead.',
129
+ )
130
+ return getLastModifiedTime(globPath)
131
+ }
132
+
133
+ export function removeFirstSlash(x) {
134
+ return x.startsWith('/') ? x.slice(1) : x
135
+ }
package/node/file.js ADDED
@@ -0,0 +1,13 @@
1
+ // Common FS stuff
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
4
+
5
+ export async function mkdir(path) {
6
+ return fs.mkdir(path, { recursive: true })
7
+ }
8
+
9
+ export async function copyFile(from, to) {
10
+ const dir = path.dirname(to)
11
+ await mkdir(dir)
12
+ await fs.copyFile(from, to)
13
+ }
package/node/hash.js ADDED
@@ -0,0 +1,5 @@
1
+ import crypto from 'node:crypto'
2
+
3
+ export function sha256Hash(string) {
4
+ return crypto.createHash('sha256').update(string).digest('hex')
5
+ }
package/node/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './common.js'
2
+ export * from './dirname.js'
3
+ export * from './file-cache.js'
4
+ export * from './hash.js'
5
+ export * from './pkce.js'
6
+ export * from './random-string.js'
package/node/pkce.js ADDED
@@ -0,0 +1,33 @@
1
+ // For Node only.
2
+ // Check the utils version for browsers.
3
+ import crypto from 'node:crypto'
4
+ import { randomString } from './random-string.js'
5
+
6
+ export async function PKCE() {
7
+ const codeVerifier = await randomString()
8
+ const codeChallenge = await getCodeChallenge(codeVerifier)
9
+
10
+ return {
11
+ state: await randomString(),
12
+ code_verifier: codeVerifier,
13
+ code_challenge: codeChallenge,
14
+ code_challenge_method: 'S256',
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Generates code challenge in node
20
+ * @param {string} verifier
21
+ * @returns string
22
+ */
23
+ async function getCodeChallenge(verifier) {
24
+ // Hash to code verifier with SHA-256
25
+ const hash = crypto.createHash('sha256').update(verifier).digest()
26
+
27
+ // // Base64url encode the hash
28
+ return hash
29
+ .toString('base64')
30
+ .replace(/\+/g, '-')
31
+ .replace(/\//g, '_')
32
+ .replace(/=/g, '')
33
+ }
@@ -0,0 +1,10 @@
1
+ // For Node only
2
+ import crypto from 'node:crypto'
3
+
4
+ export async function randomString(length = 32) {
5
+ return crypto.randomBytes(length).toString('base64')
6
+ }
7
+
8
+ export function randomStringSync(length = 32) {
9
+ return crypto.randomBytes(length).toString('hex')
10
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "@splendidlabz/utils",
3
+ "version": "1.3.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "sideEffects": false,
7
+ "exports": {
8
+ ".": "./lib/index.js",
9
+ "./lib/*": "./lib/*.js",
10
+ "./dom": "./dom/index.js",
11
+ "./dom/*": "./dom/*.js",
12
+ "./actions": "./actions/index.js",
13
+ "./actions/*": "./actions/*.js",
14
+ "./node": "./node/index.js",
15
+ "./node/*": "./node/*.js"
16
+ },
17
+ "scripts": {
18
+ "lint": "eslint . --fix",
19
+ "test": "vitest run",
20
+ "test:watch": "vitest"
21
+ },
22
+ "author": "Zell Liew <zellwk@gmail.com>",
23
+ "dependencies": {
24
+ "dompurify": "^3.1.6",
25
+ "glob-promise": "^6.0.5",
26
+ "pluralize": "^8.0.0",
27
+ "statuses": "^2.0.1"
28
+ },
29
+ "devDependencies": {
30
+ "@splendidlabz/eslint-config": "*",
31
+ "jsdom": "^24.0.0",
32
+ "np": "^8.0.4",
33
+ "vitest": "^1.4.0"
34
+ }
35
+ }