mirror of
https://github.com/Warky-Devs/artemis-kit.git
synced 2026-11-10 06:18:47 +00:00
Add object utilities and comprehensive text helpers
Add typed object selection, transformation, clone, merge, freeze, flatten and diff helpers. Add text normalization, truncation, search, masking, parsing, wrapping and formatting utilities. Document behavior and update bundle budgets.
This commit is contained in:
1 parent
538ec38632
commit
280a7eadd7
17 files changed
+1092
-352
No files matched your search
@@ -113,6 +113,71 @@ import { ... } from "@warkypublic/artemis-kit/strings"
|
||||
|
||||
---
|
||||
|
||||
### Text helpers (`strings`)
|
||||
|
||||
These functions are exported from `@warkypublic/artemis-kit/strings` and the package root.
|
||||
|
||||
| Function | Behavior |
|
||||
|---|---|
|
||||
| `truncate(text, maxLength, suffix = "…")` | Limit total character count, including suffix; clip the suffix if necessary |
|
||||
| `truncateWords(text, maxWords, suffix = "…")` | Keep whitespace-delimited words; preserve original text if it fits |
|
||||
| `slugify(text)` | Lowercase, strip decomposable accents, join Unicode letters/numbers with hyphens |
|
||||
| `escapeRegExp(text)` | Escape regex metacharacters for a literal pattern |
|
||||
| `normalizeWhitespace(text)` | Collapse whitespace to spaces and trim |
|
||||
| `stripDiacritics(text)` | Remove decomposable accents and combining marks; not transliteration |
|
||||
| `includesIgnoreCase(text, query, locale?)` | Literal substring match after locale-aware lowercasing |
|
||||
| `interpolate(template, values)` | Replace `{name}` using own properties only; missing keys remain unchanged |
|
||||
| `mask(text, options?)` | Hide characters; defaults to showing the last four |
|
||||
| `splitOnce(text, separator)` | Return `[before, after]` at the first literal separator |
|
||||
| `isBlank(text)` | True for null, undefined, empty, or whitespace-only strings |
|
||||
| `toSearchKey(text, locale?)` | Lowercase, remove accents, and normalize whitespace |
|
||||
| `humanize(text)` | Turn identifiers such as `customer_id` or `customerId` into `Customer id` |
|
||||
| `getInitials(name, max = 2)` | Uppercase the first character of up to `max` whitespace-delimited words |
|
||||
| `ensurePrefix(text, prefix)` / `ensureSuffix(text, suffix)` | Add an exact affix only when absent |
|
||||
| `removePrefix(text, prefix)` / `removeSuffix(text, suffix)` | Remove an exact affix once |
|
||||
| `substringBefore(text, separator)` / `substringAfter(text, separator)` | Extract either side of the first literal separator |
|
||||
| `countOccurrences(text, search, overlap = false)` | Count literal matches; empty search returns zero |
|
||||
| `commonPrefix(values)` | Longest shared prefix; empty list returns an empty string |
|
||||
| `wrapText(text, width)` | Wrap at whitespace, preserve explicit line breaks, leave long words intact |
|
||||
| `dedent(text)` | Remove shared literal spaces/tabs and outer blank lines |
|
||||
| `normalizeLineEndings(text)` | Convert CRLF and CR to LF |
|
||||
| `utf8ByteLength(text)` | Count UTF-8 bytes, including replacement bytes for lone surrogates |
|
||||
| `compareNatural(a, b)` | Sort numeric text naturally using the runtime locale, with empty strings last |
|
||||
|
||||
```ts
|
||||
import {
|
||||
truncate, toSearchKey, interpolate, mask, compareNatural,
|
||||
} from "@warkypublic/artemis-kit/strings"
|
||||
|
||||
truncate("Hello world", 8) // "Hello w…"
|
||||
toSearchKey(" CAFÉ au lait ") // "cafe au lait"
|
||||
interpolate("Hello {name}", { name: "Ada" }) // "Hello Ada"
|
||||
mask("123456789", { start: 2, end: 2, character: "#" }) // "12#####89"
|
||||
const files = ["file10", "file2"].sort(compareNatural) // ["file2", "file10"]
|
||||
```
|
||||
|
||||
Character-based helpers use grapheme clusters when `Intl.Segmenter` is available,
|
||||
keeping combined emoji and accented characters together. Older runtimes fall back
|
||||
to Unicode code points, which keep surrogate pairs intact but may separate combined
|
||||
characters. Length limits must be nonnegative safe integers; `wrapText` requires a
|
||||
positive width. Invalid limits throw `RangeError`. A zero truncation limit returns
|
||||
an empty string.
|
||||
|
||||
`mask` accepts `{ start?: number, end?: number, character?: string }`. The mask
|
||||
character must be one character; overlapping visible ranges leave the text visible.
|
||||
Masking changes only the displayed string. `interpolate` stringifies numbers and
|
||||
booleans, turns null/undefined values into empty strings, and does not evaluate or
|
||||
recursively substitute values. It does not escape HTML.
|
||||
|
||||
A missing separator yields `[text, ""]` from `splitOnce`; an empty separator yields
|
||||
`["", text]`. The substring helpers follow the same rules. Affixes are case-sensitive
|
||||
and empty affixes leave text unchanged. `includesIgnoreCase` does not remove accents
|
||||
or perform full Unicode case folding; use `toSearchKey` on both operands for
|
||||
accent-insensitive search. `wrapText` collapses whitespace within lines; `dedent`
|
||||
treats tabs literally rather than expanding them to visual columns.
|
||||
|
||||
---
|
||||
|
||||
### `base64`
|
||||
```ts
|
||||
import { ... } from "@warkypublic/artemis-kit/base64"
|
||||
@@ -145,6 +210,149 @@ import { ... } from "@warkypublic/artemis-kit/object"
|
||||
| `retrocycle` | `($)` | Restore circular refs from `{"$ref": PATH}` objects |
|
||||
| `stringify_json` | `(object)` | `JSON.stringify` safe for circular references |
|
||||
|
||||
#### Selecting and transforming properties
|
||||
|
||||
```ts
|
||||
import {
|
||||
pick, omit, pickBy, omitBy, mapValues, mapKeys,
|
||||
isPlainObject, hasOwn, compactObject, invert,
|
||||
} from "@warkypublic/artemis-kit/object"
|
||||
|
||||
const user = { id: 1, name: "Ada", password: "secret" }
|
||||
pick(user, ["id", "name"]) // { id: 1, name: "Ada" }
|
||||
omit(user, ["password"]) // { id: 1, name: "Ada" }
|
||||
pickBy({ a: 1, b: 0 }, value => value > 0) // { a: 1 }
|
||||
omitBy({ a: 1, b: 0 }, value => value > 0) // { b: 0 }
|
||||
mapValues({ a: 2, b: 3 }, value => value * 2) // { a: 4, b: 6 }
|
||||
mapKeys({ first: "Ada" }, key => key.toUpperCase()) // { FIRST: "Ada" }
|
||||
isPlainObject({}) // true
|
||||
isPlainObject(new Date()) // false
|
||||
hasOwn({ id: 1 }, "id") // true
|
||||
hasOwn({}, "toString") // false
|
||||
compactObject({ a: null, b: undefined, c: 0, d: false, e: "" })
|
||||
// { c: 0, d: false, e: "" }
|
||||
invert({ a: "red", b: "red", c: "blue" })
|
||||
// Map { "red" => ["a", "b"], "blue" => ["c"] }
|
||||
```
|
||||
|
||||
These helpers produce shallow results and preserve value references. They operate
|
||||
on own enumerable string and symbol properties; `pick` can also select an explicitly
|
||||
named non-enumerable own property. Results contain ordinary writable data properties,
|
||||
not the original descriptors. Accessors are read when selected or transformed.
|
||||
`mapKeys` keeps the last value when mapped keys collide. `invert` groups duplicate
|
||||
values using Map equality, so object values are compared by identity.
|
||||
`isPlainObject` accepts objects with this realm's `Object.prototype` or a null
|
||||
prototype. Class instances and objects from another realm are not plain objects.
|
||||
Keys such as `__proto__` are copied as own data properties without changing prototypes.
|
||||
Use these helpers on data records: static types cannot describe property enumeration
|
||||
or distinguish inherited keys from own keys.
|
||||
|
||||
#### Cloning, merging, and freezing
|
||||
|
||||
```ts
|
||||
import { deepClone, deepMerge, deepFreeze } from "@warkypublic/artemis-kit/object"
|
||||
|
||||
const original = { user: { name: "Ada" }, created: new Date(0) }
|
||||
const copy = deepClone(original)
|
||||
copy.user === original.user // false
|
||||
copy.created.getTime() // 0
|
||||
|
||||
const defaults = { theme: { color: "blue", size: 12 }, tags: ["base"] }
|
||||
const overrides = { theme: { size: 14 }, tags: ["custom"] }
|
||||
deepMerge(defaults, overrides)
|
||||
// { theme: { color: "blue", size: 14 }, tags: ["custom"] }
|
||||
deepMerge(defaults, overrides, { arrays: "concat" })
|
||||
// { theme: { color: "blue", size: 14 }, tags: ["base", "custom"] }
|
||||
|
||||
const config = deepFreeze({ nested: { enabled: true }, tags: ["fixed"] })
|
||||
Object.isFrozen(config.nested) // true
|
||||
Object.isFrozen(config.tags) // true
|
||||
```
|
||||
|
||||
`deepClone` preserves cycles and shared references. It supports primitives, plain
|
||||
objects, arrays, Date, RegExp (including `lastIndex`), Map, Set, ArrayBuffer, typed
|
||||
arrays, and DataView. Buffer views share the cloned backing buffer when the originals
|
||||
share one. Sparse array holes and null prototypes are preserved. Plain objects and
|
||||
arrays copy own enumerable string/symbol values; accessors become evaluated values.
|
||||
Custom properties on built-ins are not copied. Functions, custom classes, WeakMap,
|
||||
WeakSet, SharedArrayBuffer, and other unsupported objects throw `TypeError`.
|
||||
|
||||
`deepMerge` requires plain-object roots and returns a new object, leaving both inputs
|
||||
unchanged. Nested plain objects merge recursively; source values, including undefined,
|
||||
replace other values. Arrays are replaced unless `arrays: "concat"` is selected.
|
||||
Other values use `deepClone`'s supported types. Cycles through traversed plain objects
|
||||
or arrays throw `TypeError`; shared identity across independently merged branches is
|
||||
not guaranteed. The result type is `Record<PropertyKey, unknown>` because overlapping
|
||||
values may have different types; narrow values before use.
|
||||
|
||||
`deepFreeze` freezes its input **in place** and returns it as `DeepReadonly<T>`.
|
||||
It supports cycles, plain objects, and arrays, including non-enumerable and symbol
|
||||
properties. Accessors, functions, and built-ins such as Date, Map, Set, and typed
|
||||
arrays throw `TypeError` before any part of the graph is frozen. This avoids implying
|
||||
that freezing an object also prevents mutation through built-in methods.
|
||||
|
||||
#### Flattening and changes
|
||||
|
||||
```ts
|
||||
import { flattenObject, unflattenObject, diffObjects } from "@warkypublic/artemis-kit/object"
|
||||
|
||||
const flat = flattenObject({ user: { name: "Ada" }, tags: ["admin"], empty: {} })
|
||||
// { "/user/name": "Ada", "/tags": ["admin"], "/empty": {} }
|
||||
unflattenObject(flat)
|
||||
// { user: { name: "Ada" }, tags: ["admin"], empty: {} }
|
||||
flattenObject({ "a/b": { "~key": 1 } })
|
||||
// { "/a~1b/~0key": 1 }
|
||||
|
||||
diffObjects({ user: { name: "Ada" }, old: true }, { user: { name: "Grace" }, active: true })
|
||||
// [
|
||||
// { type: "changed", path: ["user", "name"], before: "Ada", after: "Grace" },
|
||||
// { type: "removed", path: ["old"], before: true },
|
||||
// { type: "added", path: ["active"], after: true },
|
||||
// ]
|
||||
```
|
||||
|
||||
Flattening traverses only nested plain objects with enumerable string keys. Paths
|
||||
start with `/`; key characters `~` and `/` become `~0` and `~1`. Empty keys are
|
||||
supported (`"/"` is the path for an empty root key). Arrays, special objects, and
|
||||
empty objects are leaves and keep their original references. Empty roots produce
|
||||
`{}`. `unflattenObject` reconstructs plain objects without inferring arrays from
|
||||
numeric keys, and rejects invalid paths and parent/child collisions such as `/a`
|
||||
and `/a/b`. Symbol and non-enumerable properties are outside this representation.
|
||||
|
||||
`diffObjects` compares own enumerable string keys in plain objects recursively.
|
||||
Other values, including arrays and dates, use `Object.is`: distinct but equal-content
|
||||
arrays report a change. Added/removed subtrees are reported as a single entry;
|
||||
absence differs from an own property containing undefined. Paths are key arrays so
|
||||
punctuation in keys is unambiguous. Returned values retain input references.
|
||||
Flattening and diffing reject cycles through the plain objects they traverse.
|
||||
|
||||
#### TypeScript utility types
|
||||
|
||||
```ts
|
||||
import type {
|
||||
DeepPartial, DeepReadonly, DeepRequired, Mutable, KeysOfType, Prettify,
|
||||
} from "@warkypublic/artemis-kit/object"
|
||||
|
||||
type Settings = { name: string; nested: { enabled: boolean }; tags: string[] }
|
||||
const patch: DeepPartial<Settings> = { nested: {} }
|
||||
type FrozenSettings = DeepReadonly<Settings> // nested fields and arrays are readonly
|
||||
const complete: DeepRequired<{ nested?: { enabled?: boolean } }> = {
|
||||
nested: { enabled: true },
|
||||
}
|
||||
type Editable = Mutable<{ readonly id: number }> // { id: number }
|
||||
type TextKeys = KeysOfType<{ id: number; name: string }, string> // "name"
|
||||
type User = Prettify<{ id: number } & { name: string }> // expanded editor display
|
||||
```
|
||||
|
||||
`DeepPartial` and `DeepRequired` preserve tuple structure and recurse through object
|
||||
properties, map values, and set elements. Map keys are unchanged. `DeepReadonly`
|
||||
also makes map keys/values and set elements recursively readonly. These types preserve
|
||||
functions, Date, and RegExp as-is; they target data shapes, not arbitrary class
|
||||
instances. Readonly typing does not prevent mutation through Date/RegExp methods.
|
||||
`Mutable` removes only top-level readonly modifiers. `KeysOfType` selects properties
|
||||
whose entire declared value type extends the requested type. These are compile-time
|
||||
utilities, not runtime validation or freezing.
|
||||
|
||||
---
|
||||
|
||||
### `dataqueue`
|
||||
|
||||
Reference in new issue
Block a user