frontacles 0.3.0 → 0.5.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.
package/CHANGELOG.md CHANGED
@@ -6,13 +6,45 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
6
6
 
7
7
  Nothing for now.
8
8
 
9
+ <!-- Compare with [last published version](https://github.com/frontacles/frontacles/compare/0.5.0...main). -->
10
+
9
11
  <!-- ⚠️ Before a new release, make sure the documentation doesn't contain any **unreleased** mention. -->
10
12
 
11
- Compare with [last published version](https://github.com/frontacles/frontacles/compare/0.2.3...main).
13
+ ## v0.5.0 (2026-01-24)
14
+
15
+ Compare with [last published version](https://github.com/frontacles/frontacles/compare/0.4.0...0.5.0).
16
+
17
+ ### New
18
+
19
+ - Add [`setAttributes`](./README.md#setattributes), a function to update HTML attributes on any number of elements.
20
+
21
+ ### Under the hood
22
+
23
+ - Setup tests for DOM utilities using Playwright via Vitest browser mode.
24
+ - Migrate types testing from `tsd` to TSTyche.
25
+ - Bump Node version from 20 to 22.
26
+
27
+ ## v0.4.0 (2025-03-08)
28
+
29
+ Compare with [previous version](https://github.com/frontacles/frontacles/compare/0.3.0...0.4.0).
30
+
31
+ ### New
32
+
33
+ - Add [`isEmail`](./README.md#isemail) to validate emails.
34
+
35
+ ### Documentation
36
+
37
+ - It is now more explicit in types that `Email.canParse` expects a `string` or a `Stringable` (changed from `any|Email` to `any|string|Email|Stringable`).
38
+ - Rephrase email documentation.
39
+
40
+ ### Under the hood
41
+
42
+ - Centralize all benchmarks in [`./benchs`](./benchs).
43
+ - Benchmark [`Email`](./benchs/url).
12
44
 
13
45
  ## v0.3.0 (2025-03-06)
14
46
 
15
- Compare with [last published version](https://github.com/frontacles/frontacles/compare/0.2.2...0.2.3).
47
+ Compare with [previous version](https://github.com/frontacles/frontacles/compare/0.2.2...0.3.0).
16
48
 
17
49
  ### New
18
50
 
@@ -32,7 +64,7 @@ Compare with [last published version](https://github.com/frontacles/frontacles/c
32
64
  ### Under the hood
33
65
 
34
66
  - Shorten `round` by a couple of Bytes.
35
- - Benchmark [`round` implementations](./src/math/bench)
67
+ - Benchmark [`round` implementations](./benchs/math)
36
68
  - Add [Valibot test suite](./src/url/test-utils/valibot-suite.js) to `Email` (all tests are passing!).
37
69
 
38
70
  ### Documentation
@@ -43,7 +75,7 @@ Compare with [last published version](https://github.com/frontacles/frontacles/c
43
75
 
44
76
  ## v0.2.2 (2025-03-01)
45
77
 
46
- Compare with [last published version](https://github.com/frontacles/frontacles/compare/0.2.1...0.2.2).
78
+ Compare with [previous version](https://github.com/frontacles/frontacles/compare/0.2.1...0.2.2).
47
79
 
48
80
  ### Breaking
49
81
 
package/README.md CHANGED
@@ -7,13 +7,131 @@ Cool utilities for front-end development (and potentially for Node).
7
7
 
8
8
  We love tiny bits (using brotli compression):
9
9
 
10
- | categories | util | size |
11
- | --- | --- | --- |
12
- | math | [`clamp`](#clamp) | 35 B |
13
- | math | [`round`](#round) | 38 B |
14
- | string | [`capitalize`](#capitalize) | 40 B |
15
- | url | [`Email`](#email) | 173 B |
16
- | | **everything** | 252 B |
10
+ | category | util | size | description |
11
+ | --- | --- | --- | --- |
12
+ | DOM | [`setAttributes`](#setattributes) | 338 B | Updates attributes of HTML or SVG element(s). |
13
+ | math | [`clamp`](#clamp) | 35 B | Make sure a number stays in a given range. |
14
+ | math | [`round`](#round) | 38 B | Round a number to a given precision. |
15
+ | string | [`capitalize`](#capitalize) | 40 B | Capitalize the first letter of a string. |
16
+ | URL | [`isEmail`](#isemail) | 86 B | Wheither a variable is a valid email address. |
17
+ | URL | [`Email`](#email) | 173 B | An `Email` object with validation and separate access to an email username and domain. |
18
+ | | **everything** | 645 B | |
19
+
20
+ ## DOM utils
21
+
22
+ ### `setAttributes`
23
+
24
+ Updates attributes of HTML element(s).
25
+
26
+ ```js
27
+ import { setAttributes } from 'frontacles/dom'
28
+
29
+ const widget = getElementById('animal-widget')
30
+
31
+ // `<div name="Animal widget" loading="true">`
32
+ setAttributes(widget, {
33
+ name: 'Animal widget'
34
+ loading: true,
35
+ })
36
+ ```
37
+
38
+ To remove an attribute, set its value to `false`, `null` or `undefined`.
39
+
40
+ ```js
41
+ // `<div name="Animal widget">`
42
+ setAttributes(widget, { loading: false })
43
+
44
+ // `<div name="Animal widget" loading="false">`
45
+ setAttributes(widget, { loading: 'false' })
46
+ ```
47
+
48
+ `setAttributes` also accepts multiple elements (array or [HTML collection](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCollection)):
49
+
50
+ ```js
51
+ const animals = widget.getElementsbyClassName('.list-item')
52
+
53
+ // `<li class="list-item" data-cat="animals">cat</li>`
54
+ setAttributes(animals, { 'data-cat': 'animals' })
55
+ ```
56
+
57
+ When an attribute is an object, its properties are converted to `attrName-propName` attributes, making it helpful for any bulk update of `aria-*` or `data-*` attributes:
58
+
59
+ ```js
60
+ setAttributes(el, {
61
+ loading: true,
62
+ aria: {
63
+ busy: true,
64
+ live: 'polite',
65
+ },
66
+ data: {
67
+ category: 'boats',
68
+ 'max-items': 12,
69
+ },
70
+ user: {
71
+ id: 4,
72
+ name: 'Liz',
73
+ },
74
+ })
75
+ ```
76
+
77
+ The previous example gives:
78
+
79
+ ```html
80
+ <div
81
+ loading="true"
82
+ aria-busy="true" aria-live="polite"
83
+ data-category="boats" data-max-items="12"
84
+ user-id="4" user-name="Liz"
85
+ >
86
+ ```
87
+
88
+ Like for non-object attributes, using `false`, `null` or `undefined` as property value will remove the matching attribute from the HTML:
89
+
90
+ ```js
91
+ setAttributes(el, { data: { 'max-items': null }}) // no more `data-max-items`
92
+ ```
93
+
94
+ > [!NOTE]
95
+ > `data` is using [`dataset`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset) under the hood, but differs from it: `setAttributes` removes any attribute with a `null` value while `dataset` turn `null` into a string. In the previous example, `el.dataset.maxItems = null` would have given `data-max-items="null"` instead of removing the attribute.
96
+
97
+ `class` updates… CSS classes. It can be an array of CSS classes, a string containing one or more (space-separated) classes, or an object in the form of `{ className: state }`, defining which classes should be added or removed from the element.
98
+
99
+ ```js
100
+ // <div class="btn btn--xl">
101
+ setAttributes(el, { class: { btn: true, 'btn--xl': true }})
102
+
103
+ // <div class="btn btn--xl card__btn">
104
+ setAttributes(el, { class: ['card__btn'] })
105
+
106
+ // <div class="btn btn--xl card__btn card__btn--special">
107
+ setAttributes(el, { class: 'card__btn--special' })
108
+
109
+ // <div class="btn btn--xl card__btn card__btn--special card__btn--1 card__btn--2">
110
+ setAttributes(el, { class: 'card__btn--1 card__btn--2' })
111
+
112
+ // <div class="btn card__btn card__btn--special card__btn--1 card__btn--2">
113
+ setAttributes(el, { class: { 'btn--xl': false }})
114
+ ```
115
+
116
+ When `style` is an object, it behaves like [`HTMLElement.style`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/style). When it’s a string, it completely replaces the attribute.
117
+
118
+ ```js
119
+ // <div style="color: red; opacity: 0.9">
120
+ setAttributes(el, {
121
+ style: {
122
+ color: 'red',
123
+ opacity: .9
124
+ }
125
+ })
126
+
127
+ // <div style="color: red;">
128
+ setAttributes(el, { style: { opacity: null }})
129
+
130
+ // <div style="gap: 2px; opacity: .9;">
131
+ setAttributes(el, { style: 'gap: 2px; opacity: .9;')
132
+ ```
133
+
134
+ `setAttributes` also works on any SVG or descendant elements.
17
135
 
18
136
  ## Math utils
19
137
 
@@ -46,10 +164,9 @@ clamp(Infinity, 0, 10) // 10
46
164
  > [!NOTE]
47
165
  > `clamp` mostly follows [`Math.clamp` TC39 proposal](https://github.com/tc39/proposal-math-clamp), except it doesn’t throw if you flip the order of the _min_ (2nd parameter) and _max_ (3rd parameter) numbers.
48
166
 
49
-
50
167
  ### `round`
51
168
 
52
- Round a number to the (optionally) provided precision.
169
+ Round a number to the (optionally) provided decimal precision. The default precision is 0 (no decimal).
53
170
 
54
171
  ```js
55
172
  import { round } from 'frontacles/math'
@@ -66,7 +183,7 @@ round(687.3456, -1) // 690
66
183
  round(687.3456, -2) // 700
67
184
  ```
68
185
 
69
- Trying to round `Infinity` or to round a number to an _infinite_ precision is also possible:
186
+ Using `Infinity` is also possible:
70
187
 
71
188
  ```js
72
189
  round(Infinity, -2) // Infinity
@@ -97,11 +214,40 @@ capitalize('صحراء') // 'صحراء' (Arabic)
97
214
 
98
215
  ## URL utils
99
216
 
217
+ ### `isEmail`
218
+
219
+ Tells whether a string is a valid email.
220
+
221
+ ```js
222
+ isEmail('someone@domain.tld') // true
223
+ isEmail('invalid@email.com:3000') // false
224
+ ```
225
+
226
+ > [!TIP]
227
+ > Should I use `isEmail` or [`Email.canParse`](#emailcanparse) to validate emails?
228
+ >
229
+ > Short answer: use `isEmail`.
230
+ >
231
+ > <details>
232
+ > <summary>Nuanced answer</summary>
233
+ >
234
+ > Your use case:
235
+ >
236
+ > - If you **only need to validate** email addresses, use `isEmail`.
237
+ > - If you also need to be able to get or set an email username or hostname **independently**, use `Email.canParse`.
238
+ >
239
+ > When using the `Email` class, you can still use `isEmail` if you want ultra-performance (e.g. your Node API validates tons of emails per seconds) because `isEmail` is 6✕ faster, at the cost of a bit less than 100 Bytes (compressed).
240
+ >
241
+ > The reason `isEmail` is faster is that it relies on a single RegExp while `Email.canParse` uses the browser built-in, which results in a bit more of computation, but with less code. For now, it’s not planned to use `isEmail` implementation in `Email.canParse` as it would increase its size by 50 Bytes.
242
+ >
243
+ > Keep in mind that **`Email.canParse` is fast enough** for the 99% use cases. Despite their implementation difference, both behave the same and pass the same tests.
244
+ > </details>
245
+
100
246
  ### `Email`
101
247
 
102
- A class to instantiate an `Email` object or validate email addresses.
248
+ A class to instantiate an `Email` object or validate email addresses. It extends the [`URL` object](https://developer.mozilla.org/en-US/docs/Web/API/URL) and has similar predictable behaviors.
103
249
 
104
- Unlike most libraries using [RegEx to validate a string is an email](https://github.com/colinhacks/zod/blob/e2b9a5f9ac67d13ada61cd8e4b1385eb850c7592/src/types.ts#L648-L663) (which is prone to [bugs](https://github.com/colinhacks/zod/issues/3913)), Frontacles `Email` relies on the same mechanism as your browser, making it robust, and very likely RFC compliant.
250
+ #### `Email.constructor`
105
251
 
106
252
  ```js
107
253
  import { Email } from 'frontacles/url/email'
@@ -109,7 +255,23 @@ import { Email } from 'frontacles/url/email'
109
255
  const email = new Email('someone@domain.tld')
110
256
  ```
111
257
 
112
- Get or set the username and the hostname separately.
258
+ Trying to instantiate an Email with an invalid address will throw. This behaviour is similar to the [`URL` constructor](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL), since `Email` relies on it under the hood.
259
+
260
+ ```js
261
+ new Email('double@at@sign.com') // ❌ throw TypeError
262
+ ```
263
+
264
+ Another behaviour from `URL`: passing an `Email` object to the `Email` constructor or to [`Email.canParse`](#emailcanparse) is possible.
265
+
266
+ ```js
267
+ const email = new Email('someone@domain.tld')
268
+ const alsoEmail = new Email(email) // ✅ a new Email object!
269
+ Email.canParse(email) // ✅ true
270
+ ```
271
+
272
+ #### `.username` and `.hostname`
273
+
274
+ Get or set the email username and hostname separately.
113
275
 
114
276
  ```js
115
277
  email.username // 'someone'
@@ -121,35 +283,27 @@ email.hostname = 'newdomain.tld' // ✅ domain migrated
121
283
  const { username, hostname } = new Email('someone@domain.tld')
122
284
  ```
123
285
 
124
- An `Email` object is converted to a string when used along another string, or by directly calling `toString`.
286
+ #### `.toString`
287
+
288
+ In a string context, an `Email` object is automatically converted to a string, or manually by calling the `toString` method.
125
289
 
126
290
  ```js
127
291
  console.log(`email: ${email}`) // 'email: someone@newdomain.tld'
128
292
  console.log(email.toString()) // 'someone@newdomain.tld'
129
293
  ```
130
294
 
131
- Validate an email address with `Email.canParse`. It passes the complete Zod test suites, and beyond.
295
+ #### `Email.canParse`
132
296
 
133
- ```js
134
- Email.canParse('someone@domain.tld') // true
135
- Email.canParse('invalid@email.com:3000') // false
136
- ```
297
+ Validate an email address with `Email.canParse`.
137
298
 
138
- Trying to instantiate an Email with an invalid address will throw. This behaviour is similar to the [`URL` constructor](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL), since `Email` relies on it under the hood.
299
+ Unlike most libraries using [RegExp to validate a string is an email](https://github.com/colinhacks/zod/blob/e2b9a5f9ac67d13ada61cd8e4b1385eb850c7592/src/types.ts#L648-L663) (which is prone to [bugs](https://github.com/colinhacks/zod/issues/3913)), Frontacles `Email` relies on the built-in `URL` mechanisms, making it robust, and very likely RFC compliant. It passes [popular libraries test suites](./src/url/test-utils), and beyond.
139
300
 
140
301
  ```js
141
- new Email('double@at@sign.com') // ❌ throw TypeError
302
+ Email.canParse('someone@domain.tld') // true
303
+ Email.canParse('invalid@email.com:3000') // false
142
304
  ```
143
305
 
144
- Another behaviour from the `URL` class: you can pass an `Email` object to the `Email` constructor (or to `Email.canParse`, but it doesn’t really make sense).
145
-
146
- ```js
147
- const email = new Email('someone@domain.tld')
148
-
149
- const alsoEmail = new Email(email) // ✅ a new Email object!
150
-
151
- Email.canParse(email) // ✅ true
152
- ```
306
+ If `canParse` is all you need from the `Email` class, consider using [isEmail](#isemail) instead.
153
307
 
154
308
  ## Changelog
155
309
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "frontacles",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "Front-end utilities for artisans",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -12,26 +12,26 @@
12
12
  },
13
13
  "types": "./types/index.d.ts",
14
14
  "engines": {
15
- "node": ">=20.18.1"
15
+ "node": ">=22.12.0"
16
16
  },
17
17
  "scripts": {
18
18
  "types": "tsc && dts-bundle-generator --silent --no-banner=true -o types/index.d.ts types-transitive/index.d.ts",
19
19
  "posttypes": "node scripts/inject-jsdoc.mjs",
20
20
  "test": "vitest run",
21
- "test:types": "npm exec tsd",
21
+ "test:types": "tstyche",
22
22
  "test:ui": "vitest --ui --coverage.enabled --coverage.exclude=types",
23
+ "test:watch": "vitest",
23
24
  "coverage": "vitest run --coverage --coverage.exclude=types",
24
- "bench": "vitest bench",
25
- "watch": "vitest watch",
25
+ "watch": "npm run test:watch",
26
26
  "build": "echo \"Nothing to build, this command is only here to please size-limit GitHub action\" && exit 0",
27
27
  "size": "size-limit",
28
28
  "lint": "eslint",
29
- "lint-fix": "eslint --fix"
29
+ "lint:fix": "eslint --fix",
30
+ "lint:inspect": "eslint --inspect-config"
30
31
  },
31
32
  "files": [
32
33
  "CHANGELOG.md",
33
34
  "src/**/*.js",
34
- "!src/**/bench",
35
35
  "!src/**/test-utils",
36
36
  "!src/**/*.test.js",
37
37
  "types/index.d.ts"
@@ -57,16 +57,18 @@
57
57
  },
58
58
  "devDependencies": {
59
59
  "@eslint/js": "^9.9.0",
60
- "@size-limit/preset-small-lib": "^11.1.4",
61
- "@vitest/coverage-v8": "^3",
60
+ "@size-limit/preset-small-lib": "^12",
61
+ "@vitest/browser-playwright": "^4",
62
+ "@vitest/coverage-v8": "^4",
62
63
  "@vitest/eslint-plugin": "^1.0.1",
63
- "@vitest/ui": "^3",
64
+ "@vitest/ui": "^4",
64
65
  "dts-bundle-generator": "^9.5.1",
65
66
  "eslint": "^9.9.0",
66
- "size-limit": "^11.1.4",
67
- "tsd": "^0.31.1",
67
+ "globals": "^17",
68
+ "size-limit": "^12",
69
+ "tstyche": "^6.0.2",
68
70
  "typescript": "^5.5.4",
69
- "typescript-eslint": "^8.0.1",
70
- "vitest": "^3"
71
+ "typescript-eslint": "^8",
72
+ "vitest": "^4"
71
73
  }
72
74
  }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Bulk update attributes of HTML element(s).
3
+ *
4
+ * Specific behaviour for the following types of values:
5
+ *
6
+ * - boolean, `null` and `undefined`:
7
+ * - `true` adds the attribute to the HTML, with no value (`<p hidden="">`)
8
+ * - `false`, `null` and `undefined` removes the attribute from the HTML
9
+ * - objects:
10
+ * - `class` will add/remove CSS classes from `{ className: state }` entries
11
+ * - `data` is pushed to `Element.dataset`, but a `data` property that is
12
+ * `null` or `undefined` removes its `data-*` attribute from the HTML:
13
+ * `{ data: { id: 1, enabled: false }}` gives `<p data-id="1">`
14
+ * - `style` is pushed to `Element.style` (inline CSS):
15
+ * `{ style: { color: 'red', gap: '2px' }}` gives
16
+ * `<p style="color: red; gap: 2px;">`
17
+ * - all other objects are converted the same way:
18
+ * `aria: { label: 'Hello!' }` gives `<p aria-label="Hello!">`
19
+ * - `class` can also be a string with one or more classes (space-separated),
20
+ * or an array of classes.
21
+ *
22
+ * @template {Element | Element[] | HTMLCollection} T
23
+ * @param {T} elements
24
+ * @param {Attributes} attributes
25
+ * @returns {T} The received element(s). Use it for method chaining.
26
+ */
27
+ export const setAttributes = (elements, attributes) => {
28
+ const items = elements instanceof Element
29
+ ? [elements]
30
+ : [...elements]
31
+
32
+ attributes = normalizeAttributes(attributes)
33
+
34
+ items.forEach(element =>
35
+ attributes.forEach(([name, value]) => {
36
+ if (value == null) {
37
+ return element.removeAttribute(name)
38
+ }
39
+
40
+ // CSS `class`, as array or object.
41
+ if (name == 'class') {
42
+ return Array.isArray(value)
43
+ ? element.classList.add(...value)
44
+ : Object.entries(value).forEach(([className, classState]) =>
45
+ element.classList.toggle(className, classState)
46
+ )
47
+ }
48
+
49
+ // Only `data` and `style` can go in this `if`.
50
+ if (typeof value == 'object') {
51
+
52
+ // `style` attribute (inline styles)
53
+ if (name == 'style') {
54
+ return Object.assign(element.style, value)
55
+ }
56
+
57
+ // `data-*` attributes (using `dataset`)
58
+ Object.assign(element.dataset, value)
59
+
60
+ return Object.entries(value).forEach(([dataKey, dataValue]) => {
61
+ /**
62
+ * Remove `data-*` prop if their value is `null` or `undefined`,
63
+ * because `dataset` stringify them but `setAttributes` apply
64
+ * the logic of regular HTML attributes to all attributes.
65
+ */
66
+ if (dataValue == null) {
67
+ delete element.dataset[dataKey]
68
+ }
69
+ })
70
+ }
71
+
72
+ element.setAttribute(name, value)
73
+ })
74
+ )
75
+
76
+ return elements
77
+ }
78
+
79
+ /** @param {Record<string, any>} fnAttributes */
80
+ const normalizeAttributes = fnAttributes => {
81
+ const attributes = { ...fnAttributes }
82
+
83
+ /**
84
+ * Normalize object attributes.
85
+ * It turns `{ key: { name: value }}` into `'key-name="value"'`.
86
+ */
87
+ Object.entries(attributes)
88
+ .filter(([name, value]) =>
89
+ value
90
+ && typeof value == 'object'
91
+ && !['class', 'data', 'style'].includes(name)
92
+ )
93
+ .forEach(([name, value]) => {
94
+ delete attributes[name]
95
+
96
+ Object.entries(value).forEach(([attrName, attrValue]) => {
97
+ attributes[`${name}-${attrName}`] = attrValue
98
+ })
99
+ })
100
+
101
+ // Normalize `class` attribute from string to array.
102
+ if (typeof attributes.class == 'string') {
103
+ attributes.class = attributes.class.split(' ')
104
+ }
105
+
106
+ // Normalize boolean attributes: `true` becomes `''`, `false` becomes `null`.
107
+ return Object.entries(attributes)
108
+ .map(([name, value]) => [
109
+ name,
110
+ typeof value == 'boolean'
111
+ ? (value ? '' : null)
112
+ : value // not boolean
113
+ ])
114
+ }
115
+
116
+ /** @typedef {boolean|string|number|null|undefined} AttributePrimitive */
117
+ /** @typedef {AttributePrimitive | Record<string, AttributePrimitive>} AttributeValue */
118
+ /** @typedef {Record<string, AttributeValue>} BaseAttributes */
119
+ /** @typedef {{ class?: string | string[] | Record<string, boolean> }} ClassAttribute */
120
+ /** @typedef {{ style?: AttributePrimitive | CSSStyleDeclaration }} StyleAttribute */
121
+ /** @typedef {BaseAttributes | ClassAttribute | StyleAttribute} Attributes */
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ export * from './dom/index.js'
1
2
  export * from './math/index.js'
2
3
  export * from './string/index.js'
3
4
  export * from './url/email.js'
package/src/url/email.js CHANGED
@@ -17,6 +17,9 @@ export class Email extends URL {
17
17
  }
18
18
 
19
19
  /**
20
+ * The email address (`username@domain.tld`) as a string.
21
+ *
22
+ * (maintainer comment)
20
23
  * Replace the string representation of the top-level class (`URL`) to be an
21
24
  * email address instead of `ftp://username@domain.tld`. It is needed for
22
25
  * situation where type casting to string is involved (`console.log`…).
@@ -38,7 +41,7 @@ export class Email extends URL {
38
41
  /**
39
42
  * Whether or not an email address is parsable and valid.
40
43
  *
41
- * @param {any|Email} address
44
+ * @param {any|string|Email|Stringable} address
42
45
  */
43
46
  static canParse(address) {
44
47
  try {
@@ -49,3 +52,17 @@ export class Email extends URL {
49
52
  }
50
53
  }
51
54
  }
55
+
56
+ /**
57
+ * Whether or not an email address is parsable and valid.
58
+ *
59
+ * It uses WHATWG recommended RegExp: https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
60
+ *
61
+ * @param {any|string|Stringable} address
62
+ */
63
+ export const isEmail = address => /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(address)
64
+
65
+ /**
66
+ * @typedef {Object} Stringable
67
+ * @property {function(): string} toString - The object as a string.
68
+ */
package/types/index.d.ts CHANGED
@@ -1,3 +1,14 @@
1
+ export function setAttributes<T extends Element | Element[] | HTMLCollection>(elements: T, attributes: Attributes): T;
2
+ export type AttributePrimitive = boolean | string | number | null | undefined;
3
+ export type AttributeValue = AttributePrimitive | Record<string, AttributePrimitive>;
4
+ export type BaseAttributes = Record<string, AttributeValue>;
5
+ export type ClassAttribute = {
6
+ class?: string | string[] | Record<string, boolean>;
7
+ };
8
+ export type StyleAttribute = {
9
+ style?: AttributePrimitive | CSSStyleDeclaration;
10
+ };
11
+ export type Attributes = BaseAttributes | ClassAttribute | StyleAttribute;
1
12
  export function clamp(val: number, min: number, max: number): number;
2
13
  export function round(number: number, precision?: number): number;
3
14
  export function capitalize(str: string): string;
@@ -17,14 +28,21 @@ export class Email extends URL {
17
28
  /**
18
29
  * Whether or not an email address is parsable and valid.
19
30
  *
20
- * @param {any|Email} address
31
+ * @param {any|string|Email|Stringable} address
21
32
  */
22
- static canParse(address: any | Email): boolean;
33
+ static canParse(address: any | string | Email | Stringable): boolean;
23
34
  /**
24
35
  * @param {string|Email} address An email address like `someone@domain.tld`.
25
36
  */
26
37
  constructor(address: string | Email);
27
38
  #private;
28
39
  }
40
+ export function isEmail(address: any | string | Stringable): boolean;
41
+ export type Stringable = {
42
+ /**
43
+ * - The object as a string.
44
+ */
45
+ toString: () => string;
46
+ };
29
47
 
30
48
  export {};