npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@mrok-creator/unifold

v0.3.0

Published

Zero-dependency Unicode normalization for data intake: string sanitizing with an audit trail, canonical matching keys, RFC 3986-safe URL normalization, and mixed-script (homoglyph) domain detection.

Downloads

281

Readme

@mrok-creator/unifold

Zero-dependency, fully typed Unicode normalization for a data-intake / deduplication pipeline: sanitize strings for storage, build ephemeral matching keys, safely normalize URLs, and flag mixed-script ("homoglyph") domains.

  • No runtime dependencies.
  • Tree-shakeable (named exports only, sideEffects: false).
  • Deterministic: no time, randomness, locale, or I/O in any normalization function.
  • Every storage-level transform reports an audit trail (changes[]) of exactly what it changed.

Install

pnpm add @mrok-creator/unifold
# or
npm install @mrok-creator/unifold

Modules

sanitize — storage-level cleanup

Applies, in order: Cyrillic→Latin homoglyph folding, BOM removal, control-character→space replacement, zero-width-character removal, trim, and space-run collapsing. Returns the cleaned value plus an audit trail — this is the value you persist.

import { sanitize } from '@mrok-creator/unifold';

sanitize(' Offer  A ');
// { value: 'Offer A', changed: true, changes: [...] }

When you only need the cleaned string (no audit trail), use the value-only wrapper:

import { sanitizeValue } from '@mrok-creator/unifold';

sanitizeValue(' Offer  A '); // 'Offer A'

canonicalKey — matching-only key

Everything sanitize does, plus case folding and separator unification: general punctuation, dashes of all kinds, quotes of all kinds, underscore and NBSP all fold to a single space, so stylistic variants of one name converge to one key. Returns a plain string — never persist this, it's for comparison only.

import { canonicalKey } from '@mrok-creator/unifold';

canonicalKey('Offer-A') === canonicalKey('offer_a'); // true
canonicalKey('“Acme”–Co Ltd') === canonicalKey('Acme.Co Ltd'); // true → 'acme co ltd'

normalizeUrl — RFC 3986-safe URL normalization

Applies only transforms that are RFC 3986-safe or explicitly spec-mandated (duplicate-slash collapse in paths): trim + invisible-character removal, punctuation-junk stripping around a scheme:// URL, scheme/host lowercasing, default-port stripping, percent-encoding hex uppercasing (path only), and duplicate-slash collapsing (path only). Malformed input is returned as-is (with the safe text cleanups applied) — this function never throws.

import { normalizeUrl } from '@mrok-creator/unifold';

normalizeUrl('HTTP://Example.COM:80//a?utm=x');
// { value: 'http://example.com/a?utm=x', changed: true, changes: [...] }

normalizeUrl('".https://example.com".');
// { value: 'https://example.com', changed: true, changes: [{ rule: 'strip-junk', ... }] }

When you only need the normalized string (no audit trail), use the value-only wrapper:

import { normalizeUrlValue } from '@mrok-creator/unifold';

normalizeUrlValue('HTTP://Example.COM:80//a'); // 'http://example.com/a'

suspiciousDomain — mixed-script flag (detect only)

Flags a host whose letters mix scripts (e.g. Cyrillic look-alikes inside a Latin domain — pаypal.com vs paypal.com). Detection only: the host is never rewritten, because auto-fixing risks breaking a legitimate domain.

import { suspiciousDomain } from '@mrok-creator/unifold';

suspiciousDomain('pаypal.com');
// { host: 'pаypal.com', suspicious: true, reason: 'mixed-script', scripts: ['latin', 'cyrillic'] }

Note: pass the decoded Unicode host. Punycode (xn--) labels are not decoded, so an encoded look-alike domain will not be flagged. Opt-in punycode decoding is planned for a future release.

Rule tables

sanitize rule order (storage-level)

| Order | Rule id | What it does | | ----- | ----------------- | --------------------------------------------------------------------------------------------------------- | | 1 | homoglyph | Folds single-codepoint Cyrillic look-alikes to their Basic Latin letter (generated data, Unicode 16.0.0). | | 2 | bom | Removes a byte-order-mark (U+FEFF) anywhere in the string. | | 3 | control | Replaces control characters (U+0000–U+001F, U+007F) with a space. | | 4 | zero-width | Removes zero-width characters (e.g. ZWSP, ZWJ, ZWNJ). | | 5 | trim | Trims leading/trailing whitespace. | | 6 | collapse-spaces | Collapses runs of ASCII spaces (U+0020) to a single space; NBSP is never collapsed. |

canonicalKey runs all six of the above, then folds NBSP and the spec's separator punctuation to a space — general punctuation (. , ; : ! ? ( ) [ ] { } / \ | @ # $ % ^ & * + = ~), underscore, dashes of all kinds and quotes of all kinds — then case-folds, before a final trim + collapse. Stylistic variants of one name converge to a single key; characters outside the separator set pass through unchanged.

normalizeUrl rule order

| Order | Rule id | What it does | | ----- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | trim | Trims edge ASCII whitespace and NBSP (not native trim() — see "What it never touches"). | | 2 | invisible | Strips BOM, control characters and zero-width characters anywhere in the URL. | | 3 | strip-junk | Strips punctuation junk around a scheme:// URL: a leading non-alphanumeric run before the scheme, and trailing quotes / sentence punctuation (.,;:!) / unbalanced closing brackets — a balanced closer stays, so https://[::1] and …/Rust_(programming_language) are untouched while (https://a.com) sheds its wrapper. /, ? and # are never stripped from the end; plain non-URL strings, protocol-relative //host and alphanumeric prefixes (0https://…) are never touched. | | 4 | scheme-lowercase | Lowercases the scheme (HTTP://http://). | | 5 | host-lowercase | Lowercases the host. | | 6 | default-port | Strips an explicit default port (:80 for http, :443 for https). | | 7 | percent-encoding-uppercase | Uppercases percent-encoding hex triplets in the path (%2f%2F). | | 8 | collapse-slashes | Collapses runs of / in the path to one; a run immediately after : is never collapsed, so :// survives even in junk-prefixed, mis-parsed input. |

What it never touches

  • Query and fragment — never rewritten by the structural rules (no percent-encoding case changes, no slash collapsing). Edge text cleanup is the one exception: like trim with trailing whitespace, strip-junk removes trailing punctuation junk from the end of the string even when a query or fragment is present (…?q=hi. and …#frag. lose the final .; a trailing FQDN root dot in https://example.com. is stripped the same way).
  • The www. prefix — not stripped or added; www.example.com and example.com normalize independently.
  • Trailing slash/a and /a/ are left exactly as given; no trailing-slash policy is imposed.
  • Interior NBSP and underscores at the storage levelsanitize does not fold NBSP or dash/underscore variants; that folding is matching-only and happens in canonicalKey, never in the persisted value.

Type reuse

All public types are named exports re-exported from the package root, so consumers can type their own code against them instead of redeclaring shapes:

import type { NormalizationResult, SuspiciousDomainResult } from '@mrok-creator/unifold';

function persist(result: NormalizationResult): void {
  // result.value / result.changed / result.changes are fully typed
}

Full type surface: NormalizationResult, NormalizationChange, SanitizeRuleId, UrlRuleId, RuleId, SuspiciousDomainResult, SuspiciousReason.

Status

Published on npm. All four modules (sanitize, canonicalKey, normalizeUrl, suspiciousDomain) are implemented, tested (100% coverage on all four metrics), and reviewed. See CHANGELOG.md for releases and .claude/docs/ for the module map, architectural decision log, and Unicode data notes.