x51 / projects // proto

@kung-fu/proto is a small utility library that wraps JavaScript primitives in fluent helpers. The package exports five wrapper factories — s(), a(), n(), obj(), helem() — plus the standalone range() helper.

Each wrapper holds a mutable internal value, so most methods modify state in place and also return the same wrapper. The chained style is canonical, but storing a reference and calling methods imperatively works identically. The schema interfaces (IString, IArray<T>, etc.) are internal — rely on TypeScript inference at the call site rather than importing them.

 

---// install

npm install @kung-fu/proto

 

---// import

import { s, a, n, obj, helem, range } from '@kung-fu/proto';

 

---// strings // s(value: string)

Wraps a string. Chainable transforms mutate the wrapper’s value and return this; predicates and getters return primitives.

methodreturnsdescription
lower() / upper()wrappercase conversion (lower uses toLocaleLowerCase, upper uses toUpperCase)
trim() / ltrim() / rtrim()wrapperregex-based whitespace trimming
normalize()wrappercollapse runs of whitespace into single spaces, trim outer space
capFirst()wrapperuppercase the first letter; if the string starts with (, [, ", or ', uppercase the next character too
capWords()wrapperapply capFirst to each space-separated token
truncateWords(n)wrapperkeep only the first n whitespace-delimited words
truncateWordsWithHtml(n)wrappertruncate then re-emit closing tags for any opener still in scope
stripHtml()wrapperstrip tags, <script> + <style> blocks, HTML comments, and decode &nbsp; / &amp;
escapeHtml()wrapperescapes &, <, >, and "; the & rule skips already-encoded entities so the call is idempotent
slugify(lower?)wrappernormalize, then replace any non-[a-z0-9] with -; lowercases by default (lower defaults to true)
startsWith(part, pos?) / endsWith(part, pos?)booleansubstring tests; pos follows native String.prototype semantics (start offset for startsWith, length limit for endsWith)
contains(val)booleanindexOf(val) !== -1
isNullOrEmpty()booleantrue for null, undefined, or whitespace-only
toBool()booleancase-insensitive parse: true, 1, y, t, on are true; everything else is false
getValueByKey(k)stringparse a k1:v1;k2:v2 blob and return the value for k ('' if missing)
setValueByKey(k, v)new wrapperreturn a fresh wrapper with the given key’s value updated
toString()stringunwrap to a native string

Slugify a title (the chain mutates and the unwrap returns):

s('  Hello,   World!  ').slugify().toString();
      // "hello-world"

capFirst handles leading punctuation:

s('"hello there"').capFirst().toString();
      // '"Hello there"'
      
      s('(parens stay)').capFirst().toString();
      // "(Parens stay)"

Strip and truncate user-submitted HTML for a teaser:

const teaser = s(post.body)
          .stripHtml()
          .normalize()
          .truncateWords(40)
          .toString();

Truncate while keeping HTML structure:

s('<p>Lorem <em>ipsum dolor</em> sit amet</p>')
          .truncateWordsWithHtml(3)
          .toString();
      // "<p>Lorem <em>ipsum dolor</em></p>"

Truthy-string parser — useful for query-string flags:

s('on').toBool();      // true
      s('Yes').toBool();     // false  (only 'y' / 't' single letters match)
      s('1').toBool();       // true
      s('').toBool();        // false

Parse and update a semicolon-delimited key/value blob:

s('user:alice;role:admin;tz:utc').getValueByKey('role');
      // "admin"
      
      s('user:alice;role:admin').setValueByKey('role', 'guest').toString();
      // "user:alice;role:guest"

 

---// arrays // a<T>(value: T[])

Wraps an array. Generic over T at the wrapper level, but note that each’s callback types its element parameter as any.

methodreturnsdescription
empty()wrapperset the array to []
isEmpty()booleanlength === 0
each(cb)voiditerate; cb(index: number, item: any)
remove(item)wrapperremove the first occurrence (allocates a new array internally; the wrapped reference is replaced)
contains(p, strict)booleanstrict=true uses === across any element type; strict=false looks for p as a substring within string elements only
indexOfPartial(p)numberfirst index where a string element contains p (-1 if none)
toObjectArray(name)wrapperwrap each primitive (string / number / boolean) element as { [name]: value }; non-primitives pass through unchanged. Throws if name is null or undefined
toArray()T[]unwrap to a native array

Iterate with the index-first callback:

a(['alpha', 'beta', 'gamma']).each((i, item) => {
          console.log(`${i}: ${item}`);
      });

Loose substring contains versus strict equality:

const tags = ['javascript', 'typescript', 'rust'];
      
      a(tags).contains('script', false);   // true (substring match)
      a(tags).contains('script', true);    // false (no exact element)
      a(tags).contains('rust', true);      // true
      a(tags).indexOfPartial('type');      // 1

Reshape a primitive list for an API that wants [{ id }, …]:

a([1, 2, 3]).toObjectArray('id').toArray();
      // [{ id: 1 }, { id: 2 }, { id: 3 }]

 

---// numbers // n(value: number)

methodreturnsdescription
toBool()booleanfalse when the value is exactly 0; true for everything else (including negative numbers and NaN)
random(min, max)wrapperset the wrapper’s value to a random integer in [min, max] (inclusive). Throws if either argument is NaN; numeric strings are accepted because both arguments are coerced via parseInt
toNumber()numberunwrap

The wrapper is reusable — random overwrites the held value, so chaining draws new numbers from one wrapper:

const rng = n(0);
      rng.random(1, 6).toNumber();    // e.g. 4
      rng.random(1, 6).toNumber();    // e.g. 1

 

---// objects // obj(value: any)

Tiny helper for ad-hoc shallow merging. extend mutates the wrapped object in place using only its own enumerable properties (it filters via hasOwnProperty).

const settings = { theme: 'dark', size: 14 };
      
      obj(settings).extend({ size: 16, lineHeight: 1.5 }).toObject();
      // { theme: 'dark', size: 16, lineHeight: 1.5 }
      // settings is the same reference, mutated in place.

 

---// html form elements // helem(elem)

Smart wrapper around form elements. val() dispatches on nodeName + the input’s type attribute and handles each variant the way you usually want, so calling code doesn’t have to branch.

elementget behaviorset behavior
<textarea>returns .value; falls back to innerText, then innerHTML (returns '' if all empty)sets innerHTML, innerText, and .value (errors swallowed and logged)
<input type="checkbox">returns .checked (boolean)truthy val → checked, falsy → unchecked
<input type="radio">returns the value of whichever radio in the same name-group is checked, or ''queries every radio with the same name and checks the one whose value matches
<input type="date">returns .valuestrips a trailing T... ISO time component; converts MM/DD/YYYY to YYYY-MM-DD before assigning
<input type="time">returns .valuetrims HH:MM:SS down to HH:MM
<input type="file">returns .value (the path stub)no-op (browsers disallow programmatic file-input writes)
any other <input>returns .valueassigns .value
<select> singlevalue of the selected option, or '' if nonefinds the option whose value matches and selects it
<select multiple>semicolon-delimited string of selected valuessame single-select selection (matches one option only)

clean() normalizes line breaks in the element’s value to \r\n — useful before posting a textarea to a backend that expects DOS-style EOLs.

toHTMLElement() returns the underlying element if you need to drop back to vanilla DOM.

const input = document.querySelector('#email') as HTMLInputElement;
      
      const current = helem(input).val();         // string | number | boolean
      helem(input).val('user@example.com');
      
      // Set a date input from any common format:
      helem(dateInput).val('2026-05-06T12:00:00');   // becomes 2026-05-06
      helem(dateInput).val('05/06/2026');            // becomes 2026-05-06
      
      // Pick a radio in a named group:
      // <input type="radio" name="plan" value="pro">
      // <input type="radio" name="plan" value="free">
      helem(anyRadio).val('pro');
      
      // Read a multi-select as one string:
      helem(multi).val();   // "alpha;beta;gamma"

 

---// range // range(count, start?)

Returns a fresh native number[] of count sequential integers starting at start (default 1). Despite the parameter name in the source TSDoc, the first argument is the array length, not an upper bound.

range(5);        // [1, 2, 3, 4, 5]
      range(5, 10);    // [10, 11, 12, 13, 14]
      range(0);        // []

 

---// source

npmjs.com/package/@kung-fu/proto