UNPKG

nano-string-utils

Version:

Ultra-lightweight string utilities with zero dependencies

1,511 lines (1,100 loc) β€’ 46.2 kB
# nano-string-utils Ultra-lightweight string utilities with zero dependencies. Tree-shakeable, fully typed, and optimized for modern JavaScript. [![npm version](https://img.shields.io/npm/v/nano-string-utils.svg)](https://www.npmjs.com/package/nano-string-utils) [![JSR](https://jsr.io/badges/@zheruel/nano-string-utils)](https://jsr.io/@zheruel/nano-string-utils) [![Bundle Size](https://img.shields.io/bundlephobia/minzip/nano-string-utils)](https://bundlephobia.com/package/nano-string-utils) [![TypeScript](https://img.shields.io/badge/TypeScript-100%25-blue.svg)](https://www.typescriptlang.org/) [![CI/CD](https://github.com/Zheruel/nano-string-utils/actions/workflows/ci.yml/badge.svg)](https://github.com/Zheruel/nano-string-utils/actions/workflows/ci.yml) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![Node.js](https://img.shields.io/badge/Node.js-β‰₯18-brightgreen.svg)](https://nodejs.org/) [![Deno](https://img.shields.io/badge/Deno-βœ“-blue.svg)](https://deno.land) [![Bun](https://img.shields.io/badge/Bun-βœ“-orange.svg)](https://bun.sh) ## Documentation πŸ“š **[View Full API Documentation](https://zheruel.github.io/nano-string-utils/)** πŸš€ **[Migration Guide](https://zheruel.github.io/nano-string-utils/#migration)** - Step-by-step guide for migrating from lodash/underscore ## Features - πŸš€ **Zero dependencies** - No bloat, just pure functions - πŸ“¦ **< 1KB per function** - Minimal bundle impact - 🌳 **Tree-shakeable** - Only import what you need - πŸ’ͺ **Fully typed** - Complete TypeScript support with function overloads and template literal types - ⚑ **Fast performance** - 2-25x faster than lodash for many operations - ⚑ **ESM & CJS** - Works everywhere - πŸ§ͺ **100% tested** - Reliable and production-ready - πŸ”’ **Type-safe** - Written in strict TypeScript with enhanced type inference and compile-time transformations - πŸ›‘οΈ **Null-safe** - All functions handle null/undefined gracefully without throwing errors - πŸ“ **Well documented** - JSDoc comments for all functions ## Runtime Compatibility nano-string-utils works seamlessly across all modern JavaScript runtimes: | Runtime | Support | Installation | CLI | | ----------- | ------- | ------------------------------------- | --- | | Node.js β‰₯18 | βœ… Full | `npm install nano-string-utils` | βœ… | | Deno | βœ… Full | `deno add @zheruel/nano-string-utils` | βœ… | | Bun | βœ… Full | `bun add nano-string-utils` | βœ… | | Browser | βœ… Full | Via bundler or CDN | ❌ | All core functions use standard JavaScript APIs and work identically across runtimes. The CLI tool supports Node.js, Deno, and Bun. ## Installation ### Node.js ```bash npm install nano-string-utils # or yarn add nano-string-utils # or pnpm add nano-string-utils ``` ### Deno ```typescript // From JSR (recommended) import { slugify } from "jsr:@zheruel/nano-string-utils"; // Or add to your project deno add @zheruel/nano-string-utils ``` ### Bun ```bash bun add nano-string-utils ``` ### Browser (CDN) ```html <!-- Latest version --> <script src="https://unpkg.com/nano-string-utils/dist/index.iife.js"></script> <!-- Or specific version --> <script src="https://cdn.jsdelivr.net/npm/nano-string-utils@0.16.0/dist/index.iife.js"></script> <script> // All functions available on global nanoStringUtils object const slug = nanoStringUtils.slugify("Hello World!"); console.log(slug); // 'hello-world' </script> ``` For modern browsers with ES modules: ```html <script type="module"> import { slugify, camelCase, } from "https://unpkg.com/nano-string-utils/dist/index.js"; console.log(slugify("Hello World")); // 'hello-world' console.log(camelCase("hello-world")); // 'helloWorld' </script> ``` ## Quick Start ```javascript import { slugify, camelCase, truncate, isEmail, fuzzyMatch, } from "nano-string-utils"; // Transform strings with ease slugify("Hello World!"); // 'hello-world' camelCase("hello-world"); // 'helloWorld' truncate("Long text here", 10); // 'Long te...' // Validate inputs isEmail("user@example.com"); // true isEmail("invalid.email"); // false // Advanced string matching fuzzyMatch("gto", "goToLine"); // { matched: true, score: 0.546 } fuzzyMatch("abc", "xyz"); // null (no match) ``` ### Most Popular Functions #### `slugify(str: string): string` Convert any string to a URL-safe slug. ```javascript slugify("Hello World!"); // 'hello-world' slugify(" Multiple Spaces "); // 'multiple-spaces' slugify("Special@#Characters!"); // 'special-characters' ``` #### `camelCase(str: string): string` Convert strings to camelCase for JavaScript variables. ```javascript camelCase("hello-world"); // 'helloWorld' camelCase("HELLO_WORLD"); // 'helloWorld' camelCase("Hello World"); // 'helloWorld' ``` #### `truncate(str: string, length: number, suffix?: string): string` Intelligently truncate text with customizable suffix. ```javascript truncate("Long text here", 10); // 'Long te...' truncate("Long text here", 10, "β†’"); // 'Long texβ†’' ``` #### `isEmail(str: string): boolean` Validate email addresses with a robust regex. ```javascript isEmail("user@example.com"); // true isEmail("test@sub.domain.com"); // true isEmail("invalid.email"); // false ``` #### `fuzzyMatch(query: string, target: string): FuzzyMatchResult | null` Perform fuzzy string matching for search features. ```javascript fuzzyMatch("usrctrl", "userController.js"); // { matched: true, score: 0.444 } fuzzyMatch("of", "openFile"); // { matched: true, score: 0.75 } ``` > πŸ“– **See all 44 functions in the API Reference below** ## CLI Nano String Utils includes a command-line interface for quick string transformations directly in your terminal. The CLI works with Node.js, Deno, and Bun! ### Installation & Usage #### Node.js ```bash # Global installation npm install -g nano-string-utils nano-string slugify "Hello World" # Using npx (no installation required) npx nano-string-utils slugify "Hello World" ``` #### Deno ```bash # Direct execution (no installation required) deno run --allow-read https://unpkg.com/nano-string-utils/bin/nano-string.js slugify "Hello World" # Or if installed locally deno run --allow-read node_modules/nano-string-utils/bin/nano-string.js slugify "Hello World" ``` #### Bun ```bash # Using bunx (no installation required) bunx nano-string-utils slugify "Hello World" # Or if installed bun run nano-string-utils slugify "Hello World" ``` ### Basic Usage ```bash nano-string <function> <input> [options] ``` ### Examples #### Simple transformations ```bash nano-string slugify "Hello World!" # hello-world nano-string camelCase "hello-world" # helloWorld nano-string kebabCase "hello_world" # hello-world nano-string capitalize "hello" # Hello nano-string reverse "hello" # olleh ``` #### Using pipes ```bash echo "Hello World" | nano-string slugify # hello-world cat file.txt | nano-string truncate --length 50 ``` #### Functions with options ```bash # Truncate with custom length nano-string truncate "Long text here" --length 10 # Long te... # Template interpolation nano-string template "Hello {{name}}" --data '{"name":"World"}' # Hello World # Pad strings nano-string padStart "hi" --length 5 --char "*" # ***hi # Generate random strings nano-string randomString --length 10 # Generates 10-character string # Text processing nano-string smartSplit "Dr. Smith went to the store. He bought milk." # ['Dr. Smith went to the store.', 'He bought milk.'] nano-string humanizeList "apple,banana,orange" --conjunction "or" # apple, banana, or orange ``` #### Validation functions ```bash nano-string isEmail "test@example.com" # true nano-string isUrl "https://example.com" # true nano-string isASCII "hello" # true ``` #### Analysis functions ```bash nano-string wordCount "hello world test" # 3 nano-string levenshtein "kitten" "sitting" # 3 nano-string diff "hello" "hallo" # Shows differences ``` ### Available Commands To see all available functions: ```bash nano-string --help ``` For help on a specific function: ```bash nano-string slugify --help ``` ## API Reference The library provides 44 string utility functions organized by category. Click on any category to explore the available functions. <details> <summary><b>πŸ”€ Case Conversion Functions (10 functions)</b></summary> ### Case Conversion Transform strings between different naming conventions commonly used in programming. #### `slugify(str: string): string` Converts a string to a URL-safe slug. ```javascript slugify("Hello World!"); // 'hello-world' slugify(" Multiple Spaces "); // 'multiple-spaces' ``` #### `camelCase(str: string): string` Converts a string to camelCase. ```javascript camelCase("hello world"); // 'helloWorld' camelCase("hello-world"); // 'helloWorld' camelCase("hello_world"); // 'helloWorld' ``` #### `snakeCase(str: string): string` Converts a string to snake_case. ```javascript snakeCase("hello world"); // 'hello_world' snakeCase("helloWorld"); // 'hello_world' snakeCase("hello-world"); // 'hello_world' ``` #### `kebabCase(str: string): string` Converts a string to kebab-case. ```javascript kebabCase("hello world"); // 'hello-world' kebabCase("helloWorld"); // 'hello-world' kebabCase("hello_world"); // 'hello-world' ``` #### `pascalCase(str: string): string` Converts a string to PascalCase. ```javascript pascalCase("hello world"); // 'HelloWorld' pascalCase("hello-world"); // 'HelloWorld' pascalCase("hello_world"); // 'HelloWorld' ``` #### `constantCase(str: string): string` Converts a string to CONSTANT_CASE. ```javascript constantCase("hello world"); // 'HELLO_WORLD' constantCase("helloWorld"); // 'HELLO_WORLD' constantCase("hello-world"); // 'HELLO_WORLD' constantCase("XMLHttpRequest"); // 'XML_HTTP_REQUEST' ``` #### `dotCase(str: string): string` Converts a string to dot.case. ```javascript dotCase("hello world"); // 'hello.world' dotCase("helloWorld"); // 'hello.world' dotCase("hello-world"); // 'hello.world' dotCase("XMLHttpRequest"); // 'xml.http.request' dotCase("com/example/package"); // 'com.example.package' ``` #### `pathCase(str: string): string` Converts a string to path/case (forward slash separated). ```javascript pathCase("hello world"); // 'hello/world' pathCase("helloWorld"); // 'hello/world' pathCase("hello-world"); // 'hello/world' pathCase("hello_world"); // 'hello/world' pathCase("XMLHttpRequest"); // 'xml/http/request' pathCase("src.components.Header"); // 'src/components/header' pathCase("com.example.package"); // 'com/example/package' ``` #### `titleCase(str: string, options?: { exceptions?: string[] }): string` Converts a string to title case with proper capitalization rules. ```javascript titleCase("the quick brown fox"); // 'The Quick Brown Fox' titleCase("a tale of two cities"); // 'A Tale of Two Cities' titleCase("mother-in-law"); // 'Mother-in-Law' titleCase("don't stop believing"); // "Don't Stop Believing" titleCase("NASA launches rocket"); // 'NASA Launches Rocket' titleCase("2001: a space odyssey"); // '2001: A Space Odyssey' // With custom exceptions titleCase("the lord of the rings", { exceptions: ["versus"], }); // 'The Lord of the Rings' ``` #### `sentenceCase(str: string): string` Converts a string to sentence case (first letter of each sentence capitalized). ```javascript sentenceCase("hello world"); // 'Hello world' sentenceCase("HELLO WORLD"); // 'Hello world' sentenceCase("hello. world! how are you?"); // 'Hello. World! How are you?' sentenceCase("this is the first. this is the second."); // 'This is the first. This is the second.' sentenceCase("the u.s.a. is large"); // 'The u.s.a. is large' sentenceCase("i love javascript"); // 'I love javascript' sentenceCase("what? when? where?"); // 'What? When? Where?' ``` </details> <details> <summary><b>βœ‚οΈ String Manipulation (11 functions)</b></summary> ### String Manipulation Essential functions for transforming and manipulating text content. #### `capitalize(str: string): string` Capitalizes the first letter of a string and lowercases the rest. ```javascript capitalize("hello world"); // 'Hello world' capitalize("HELLO"); // 'Hello' ``` #### `reverse(str: string): string` Reverses a string. ```javascript reverse("hello"); // 'olleh' reverse("world"); // 'dlrow' ``` #### `truncate(str: string, length: number, suffix?: string): string` Truncates a string to a specified length with an optional suffix. ```javascript truncate("Long text here", 10); // 'Long te...' truncate("Long text here", 10, "β†’"); // 'Long texβ†’' ``` #### `excerpt(str: string, length: number, suffix?: string): string` Creates a smart excerpt from text with word boundary awareness. ```javascript excerpt("The quick brown fox jumps over the lazy dog", 20); // 'The quick brown fox...' excerpt("Hello world. This is a test.", 15); // 'Hello world...' excerpt("Long technical documentation text here", 25, "…"); // 'Long technical…' excerpt("Supercalifragilisticexpialidocious", 10); // 'Supercalif...' ``` #### `pad(str: string, length: number, chars?: string): string` Pads a string to a given length by adding characters to both sides (centers the string). ```javascript pad("Hi", 6); // ' Hi ' pad("Hi", 6, "-"); // '--Hi--' pad("Hi", 7, "-"); // '--Hi---' ``` #### `padStart(str: string, length: number, chars?: string): string` Pads a string to a given length by adding characters to the left. ```javascript padStart("5", 3, "0"); // '005' padStart("Hi", 5, "."); // '...Hi' padStart("Hi", 6, "=-"); // '=-=-Hi' ``` #### `padEnd(str: string, length: number, chars?: string): string` Pads a string to a given length by adding characters to the right. ```javascript padEnd("Hi", 5, "."); // 'Hi...' padEnd("Hi", 6, "=-"); // 'Hi=-=-' padEnd("5", 3, "0"); // '500' ``` #### `deburr(str: string): string` Removes diacritics/accents from Latin characters. ```javascript deburr("cafΓ©"); // 'cafe' deburr("naΓ―ve"); // 'naive' deburr("BjΓΈrn"); // 'Bjorn' deburr("SΓ£o Paulo"); // 'Sao Paulo' deburr("MΓΌller"); // 'Muller' ``` #### `wordCount(str: string): number` Counts the number of words in a string. ```javascript wordCount("Hello world test"); // 3 wordCount("One-word counts as one"); // 5 ``` #### `randomString(length: number, charset?: string): string` Generates a random string of specified length. ```javascript randomString(10); // 'aBc123XyZ9' randomString(5, "abc"); // 'abcab' randomString(8, "0123456789"); // '42318765' ``` #### `hashString(str: string): number` Generates a simple hash from a string (non-cryptographic). ```javascript hashString("hello"); // 99162322 hashString("world"); // 113318802 ``` </details> <details> <summary><b>πŸ“ Text Processing (11 functions)</b></summary> ### Text Processing Advanced text processing utilities for handling HTML, whitespace, special characters, and entity extraction. #### `stripHtml(str: string): string` Removes HTML tags from a string. ```javascript stripHtml("<p>Hello <b>world</b>!</p>"); // 'Hello world!' stripHtml("<div>Text</div>"); // 'Text' ``` #### `escapeHtml(str: string): string` Escapes HTML special characters. ```javascript escapeHtml('<div>Hello & "world"</div>'); // '&lt;div&gt;Hello &amp; &quot;world&quot;&lt;/div&gt;' escapeHtml("It's <b>bold</b>"); // 'It&#x27;s &lt;b&gt;bold&lt;/b&gt;' ``` #### `normalizeWhitespace(str: string, options?: NormalizeWhitespaceOptions): string` Normalizes various Unicode whitespace characters to regular spaces. ```javascript normalizeWhitespace("hello world"); // 'hello world' normalizeWhitespace("hello\u00A0world"); // 'hello world' (non-breaking space) normalizeWhitespace(" hello "); // 'hello' normalizeWhitespace("hello\n\nworld"); // 'hello world' // With options normalizeWhitespace(" hello ", { trim: false }); // ' hello ' normalizeWhitespace("a b", { collapse: false }); // 'a b' normalizeWhitespace("hello\n\nworld", { preserveNewlines: true }); // 'hello\n\nworld' // Handles various Unicode spaces normalizeWhitespace("cafΓ©\u2003test"); // 'cafΓ© test' (em space) normalizeWhitespace("hello\u200Bworld"); // 'hello world' (zero-width space) normalizeWhitespace("ζ—₯本\u3000θͺž"); // 'ζ—₯本 θͺž' (ideographic space) ``` #### `removeNonPrintable(str: string, options?: RemoveNonPrintableOptions): string` Removes non-printable control characters and formatting characters from strings. ```javascript removeNonPrintable("hello\x00world"); // 'helloworld' (removes NULL character) removeNonPrintable("hello\nworld"); // 'helloworld' (removes newline by default) removeNonPrintable("hello\u200Bworld"); // 'helloworld' (removes zero-width space) removeNonPrintable("hello\u202Dworld"); // 'helloworld' (removes directional override) // With options removeNonPrintable("hello\nworld", { keepNewlines: true }); // 'hello\nworld' removeNonPrintable("hello\tworld", { keepTabs: true }); // 'hello\tworld' removeNonPrintable("hello\r\nworld", { keepCarriageReturns: true }); // 'hello\rworld' // Preserves emoji with zero-width joiners removeNonPrintable("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦"); // 'πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦' (family emoji preserved) removeNonPrintable("text\x1B[32mgreen\x1B[0m"); // 'text[32mgreen[0m' (ANSI escapes removed) ``` #### `toASCII(str: string, options?: { placeholder?: string }): string` Converts a string to ASCII-safe representation by removing diacritics, converting common Unicode symbols, and optionally replacing non-ASCII characters. ```javascript toASCII("cafΓ©"); // 'cafe' toASCII("Hello "world""); // 'Hello "world"' toASCII("emβ€”dash"); // 'em-dash' toASCII("€100"); // 'EUR100' toASCII("Β½ + ΒΌ = ΒΎ"); // '1/2 + 1/4 = 3/4' toASCII("β†’ ← ↑ ↓"); // '-> <- ^ v' toASCII("Ξ± Ξ² Ξ³"); // 'a b g' toASCII("ΠŸΡ€ΠΈΠ²Π΅Ρ‚"); // 'Privet' toASCII("δ½ ε₯½"); // '' (removes non-convertible characters) toASCII("δ½ ε₯½", { placeholder: "?" }); // '??' toASCII("Hello δΈ–η•Œ", { placeholder: "?" }); // 'Hello ??' toASCII("Β© 2024 MΓΌllerβ„’"); // '(c) 2024 Muller(TM)' ``` #### `pluralize(word: string, count?: number): string` Converts a singular word to its plural form using English pluralization rules. Optionally takes a count to conditionally pluralize. ```javascript pluralize("box"); // 'boxes' pluralize("baby"); // 'babies' pluralize("person"); // 'people' pluralize("analysis"); // 'analyses' pluralize("cactus"); // 'cacti' // With count parameter pluralize("item", 1); // 'item' (singular for count of 1) pluralize("item", 0); // 'items' (plural for count of 0) pluralize("item", 5); // 'items' (plural for count > 1) // Preserves casing pluralize("Box"); // 'Boxes' pluralize("PERSON"); // 'PEOPLE' ``` #### `singularize(word: string): string` Converts a plural word to its singular form using English singularization rules. ```javascript singularize("boxes"); // 'box' singularize("babies"); // 'baby' singularize("people"); // 'person' singularize("analyses"); // 'analysis' singularize("cacti"); // 'cactus' singularize("data"); // 'datum' // Preserves casing singularize("Boxes"); // 'Box' singularize("PEOPLE"); // 'PERSON' ``` #### `extractEntities(text: string): ExtractedEntities` Extracts various entities from text including emails, URLs, mentions, hashtags, phones, dates, and prices. ```javascript // Extract from mixed content const text = "Contact @john at john@example.com or call (555) 123-4567. Check #updates at https://example.com. Price: $99.99"; const entities = extractEntities(text); // Returns: // { // emails: ['john@example.com'], // urls: ['https://example.com'], // mentions: ['@john'], // hashtags: ['#updates'], // phones: ['(555) 123-4567'], // dates: [], // prices: ['$99.99'] // } // Extract from social media content const tweet = "Hey @alice and @bob! Check out #javascript #typescript at https://github.com/example"; const social = extractEntities(tweet); // social.mentions: ['@alice', '@bob'] // social.hashtags: ['#javascript', '#typescript'] // social.urls: ['https://github.com/example'] // Extract contact information const contact = "Email: support@company.com, Phone: +1-800-555-0100"; const info = extractEntities(contact); // info.emails: ['support@company.com'] // info.phones: ['+1-800-555-0100'] // Extract dates and prices const invoice = "Invoice Date: 2024-01-15, Due: 01/30/2024, Amount: $1,234.56"; const billing = extractEntities(invoice); // billing.dates: ['2024-01-15', '01/30/2024'] // billing.prices: ['$1,234.56'] ``` #### `smartSplit(text: string): string[]` Intelligently splits text into sentences while properly handling abbreviations, ellipses, and decimal numbers. ```javascript // Basic sentence splitting smartSplit("Hello world. How are you? I'm fine!"); // ['Hello world.', 'How are you?', "I'm fine!"] // Handles abbreviations correctly smartSplit("Dr. Smith went to the store. He bought milk."); // ['Dr. Smith went to the store.', 'He bought milk.'] // Preserves decimal numbers smartSplit("The price is $10.50. That's expensive!"); // ['The price is $10.50.', "That's expensive!"] // Handles multiple abbreviations smartSplit( "Mr. and Mrs. Johnson live on St. Paul Ave. They moved from the U.S.A. last year." ); // ['Mr. and Mrs. Johnson live on St. Paul Ave.', 'They moved from the U.S.A. last year.'] // Handles ellipses smartSplit("I was thinking... Maybe we should go. What do you think?"); // ['I was thinking...', 'Maybe we should go.', 'What do you think?'] ``` #### `humanizeList(items: unknown[], options?: HumanizeListOptions): string` Converts an array into a grammatically correct, human-readable list with proper conjunctions and optional Oxford comma. ```javascript // Basic usage humanizeList(["apple", "banana", "orange"]); // 'apple, banana, and orange' // Two items humanizeList(["yes", "no"]); // 'yes and no' // With custom conjunction humanizeList(["red", "green", "blue"], { conjunction: "or" }); // 'red, green, or blue' // Without Oxford comma humanizeList(["a", "b", "c"], { oxford: false }); // 'a, b and c' // With quotes humanizeList(["run", "jump", "swim"], { quotes: true }); // '"run", "jump", and "swim"' // Handles mixed types and nulls humanizeList([1, null, "text", undefined, true]); // '1, text, and true' // Empty arrays humanizeList([]); // '' ``` </details> <details> <summary><b>βœ… Validation Functions (4 functions)</b></summary> ### String Validation Utilities for validating string formats and content. #### `isEmail(str: string): boolean` Validates if a string is a valid email format. ```javascript isEmail("user@example.com"); // true isEmail("invalid.email"); // false isEmail("test@sub.domain.com"); // true ``` #### `isUrl(str: string): boolean` Validates if a string is a valid URL format. ```javascript isUrl("https://example.com"); // true isUrl("http://localhost:3000"); // true isUrl("not a url"); // false isUrl("ftp://files.com/file.zip"); // true ``` #### `isASCII(str: string): boolean` Checks if a string contains only ASCII characters (code points 0-127). ```javascript isASCII("Hello World!"); // true isASCII("cafΓ©"); // false isASCII("πŸ‘"); // false isASCII("abc123!@#"); // true isASCII(""); // true ``` </details> <details> <summary><b>πŸ” String Analysis & Comparison (6 functions)</b></summary> ### String Analysis & Comparison Advanced utilities for analyzing and comparing strings. #### `diff(oldStr: string, newStr: string): string` Computes a simple string diff comparison showing additions and deletions. ```javascript diff("hello world", "hello beautiful world"); // 'hello {+beautiful +}world' diff("goodbye world", "hello world"); // '[-goodbye-]{+hello+} world' diff("v1.0.0", "v1.1.0"); // 'v1.[-0-]{+1+}.0' diff("debug: false", "debug: true"); // 'debug: [-fals-]{+tru+}e' diff("user@example.com", "admin@example.com"); // '[-user-]{+admin+}@example.com' // Form field changes diff("John Doe", "Jane Doe"); // 'J[-ohn-]{+ane+} Doe' // Configuration changes diff("port: 3000", "port: 8080"); // 'port: [-300-]{+808+}0' // File extension changes diff("app.js", "app.ts"); // 'app.[-j-]{+t+}s' // No changes diff("test", "test"); // 'test' // Complete replacement diff("hello", "world"); // '[-hello-]{+world+}' ``` Uses a simple prefix/suffix algorithm optimized for readability. The output format uses: - `[-text-]` for deleted text - `{+text+}` for added text #### `levenshtein(a: string, b: string, maxDistance?: number): number` Calculates the Levenshtein distance (edit distance) between two strings. Optimized with space-efficient algorithm and early termination support. ```javascript levenshtein("cat", "bat"); // 1 (substitution) levenshtein("cat", "cats"); // 1 (insertion) levenshtein("cats", "cat"); // 1 (deletion) levenshtein("kitten", "sitting"); // 3 levenshtein("example", "exmaple"); // 2 (transposition) // With maxDistance for early termination levenshtein("hello", "helicopter", 3); // Infinity (exceeds max) levenshtein("hello", "hallo", 3); // 1 (within max) // Unicode support levenshtein("cafΓ©", "cafe"); // 1 levenshtein("πŸ˜€", "πŸ˜ƒ"); // 1 ``` #### `levenshteinNormalized(a: string, b: string): number` Calculates normalized Levenshtein similarity score between 0 and 1. Perfect for fuzzy matching and similarity scoring. ```javascript levenshteinNormalized("hello", "hello"); // 1 (identical) levenshteinNormalized("cat", "bat"); // 0.667 (fairly similar) levenshteinNormalized("hello", "world"); // 0.2 (dissimilar) levenshteinNormalized("", "abc"); // 0 (completely different) // Real-world typo detection levenshteinNormalized("necessary", "neccessary"); // 0.9 levenshteinNormalized("example", "exmaple"); // 0.714 // Fuzzy matching (common threshold: 0.8) const threshold = 0.8; levenshteinNormalized("test", "tests") >= threshold; // true (0.8) levenshteinNormalized("hello", "goodbye") >= threshold; // false (0.143) ``` #### `fuzzyMatch(query: string, target: string, options?: FuzzyMatchOptions): FuzzyMatchResult | null` Performs fuzzy string matching with a similarity score, ideal for command palettes, file finders, and search-as-you-type features. ```javascript // Basic usage fuzzyMatch("gto", "goToLine"); // { matched: true, score: 0.546 } fuzzyMatch("usrctrl", "userController.js"); // { matched: true, score: 0.444 } fuzzyMatch("abc", "xyz"); // null (no match) // Command palette style matching fuzzyMatch("of", "openFile"); // { matched: true, score: 0.75 } fuzzyMatch("svf", "saveFile"); // { matched: true, score: 0.619 } // File finder matching fuzzyMatch("index", "src/components/index.html"); // { matched: true, score: 0.262 } fuzzyMatch("app.js", "src/app.js"); // { matched: true, score: 0.85 } // Case sensitivity fuzzyMatch("ABC", "abc"); // { matched: true, score: 0.95 } fuzzyMatch("ABC", "abc", { caseSensitive: true }); // null // Minimum score threshold fuzzyMatch("ab", "a" + "x".repeat(50) + "b", { threshold: 0.5 }); // null (score too low) // Acronym matching (matches at word boundaries score higher) fuzzyMatch("uc", "UserController"); // { matched: true, score: 0.75 } fuzzyMatch("gc", "getUserController"); // { matched: true, score: 0.75 } ``` Options: - `caseSensitive` - Enable case-sensitive matching (default: false) - `threshold` - Minimum score to consider a match (default: 0) Returns: - `{ matched: true, score: number }` - When match found (score between 0-1) - `{ matched: false, score: 0 }` - For empty query - `null` - When no match found or score below threshold Scoring algorithm prioritizes: - Exact matches (1.0) - Prefix matches (β‰₯0.85) - Consecutive character matches - Matches at word boundaries (camelCase, snake_case, kebab-case, etc.) - Early matches in the string - Acronym-style matches #### `highlight(str: string, terms: string | string[], options?: HighlightOptions): string` Highlights search terms in text by wrapping them with markers. ```javascript highlight("The quick brown fox", "quick"); // 'The <mark>quick</mark> brown fox' highlight("Hello WORLD", "world"); // '<mark>Hello</mark> <mark>WORLD</mark>' (case-insensitive by default) // Multiple terms highlight("The quick brown fox", ["quick", "fox"]); // 'The <mark>quick</mark> brown <mark>fox</mark>' // Custom wrapper highlight("Error: Connection failed", ["error", "failed"], { wrapper: ["**", "**"], }); // '**Error**: Connection **failed**' // Whole word matching highlight("Java and JavaScript", "Java", { wholeWord: true }); // '<mark>Java</mark> and JavaScript' // With CSS class highlight("Hello world", "Hello", { className: "highlight" }); // '<mark class="highlight">Hello</mark> world' // HTML escaping for security highlight("<div>Hello</div>", "Hello", { escapeHtml: true }); // '&lt;div&gt;<mark>Hello</mark>&lt;/div&gt;' // Case-sensitive matching highlight("Hello hello", "hello", { caseSensitive: true }); // 'Hello <mark>hello</mark>' ``` Options: - `caseSensitive` - Enable case-sensitive matching (default: false) - `wholeWord` - Match whole words only (default: false) - `wrapper` - Custom wrapper tags (default: ['<mark>', '</mark>']) - `className` - CSS class for mark tags - `escapeHtml` - Escape HTML in text before highlighting (default: false) </details> <details> <summary><b>🌍 Unicode & International (5 functions)</b></summary> ### Unicode & International Handle complex Unicode characters, emoji, and international text. #### `graphemes(str: string): string[]` Splits a string into an array of grapheme clusters, properly handling emojis, combining characters, and complex Unicode. ```javascript graphemes("hello"); // ['h', 'e', 'l', 'l', 'o'] graphemes("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦πŸŽˆ"); // ['πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦', '🎈'] graphemes("cafΓ©"); // ['c', 'a', 'f', 'Γ©'] graphemes("πŸ‘πŸ½"); // ['πŸ‘πŸ½'] - emoji with skin tone graphemes("πŸ‡ΊπŸ‡Έ"); // ['πŸ‡ΊπŸ‡Έ'] - flag emoji graphemes("helloπŸ‘‹world"); // ['h', 'e', 'l', 'l', 'o', 'πŸ‘‹', 'w', 'o', 'r', 'l', 'd'] ``` #### `codePoints(str: string): number[]` Converts a string into an array of Unicode code points, properly handling surrogate pairs and complex characters. ```javascript codePoints("hello"); // [104, 101, 108, 108, 111] codePoints("πŸ‘"); // [128077] codePoints("€"); // [8364] codePoints("Hello πŸ‘‹"); // [72, 101, 108, 108, 111, 32, 128075] codePoints("aπŸ‘b"); // [97, 128077, 98] codePoints("πŸ‘¨β€πŸ‘©β€πŸ‘§β€πŸ‘¦"); // [128104, 8205, 128105, 8205, 128103, 8205, 128102] ``` </details> <details> <summary><b>🎯 Templates & Interpolation (2 functions)</b></summary> ### Templates & Interpolation Safe and flexible string template interpolation. #### `template(str: string, data: Record<string, any>, options?: TemplateOptions): string` Interpolates variables in a template string. ```javascript template("Hello {{name}}!", { name: "World" }); // 'Hello World!' template("{{user.name}} is {{user.age}}", { user: { name: "Alice", age: 30 }, }); // 'Alice is 30' template( "Hello ${name}!", { name: "World" }, { delimiters: ["${", "}"], } ); // 'Hello World!' ``` #### `templateSafe(str: string, data: Record<string, any>, options?: TemplateOptions): string` Interpolates variables with HTML escaping for safe output. ```javascript templateSafe("Hello {{name}}!", { name: '<script>alert("XSS")</script>', }); // 'Hello &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;!' ``` </details> <details> <summary><b>⚑ Performance Utilities (1 function)</b></summary> ### Performance Utilities Optimize expensive string operations with caching. #### `memoize<T>(fn: T, options?: MemoizeOptions): T` Creates a memoized version of a function with LRU (Least Recently Used) cache eviction. Ideal for optimizing expensive string operations like `levenshtein`, `fuzzyMatch`, or `diff` when processing repetitive data. ```javascript import { levenshtein, memoize } from "nano-string-utils"; // Basic usage - memoize expensive string operations const memoizedLevenshtein = memoize(levenshtein); // First call computes the result memoizedLevenshtein("kitten", "sitting"); // 3 (computed) // Subsequent calls with same arguments return cached result memoizedLevenshtein("kitten", "sitting"); // 3 (cached - instant) // Custom cache size (default is 100) const limited = memoize(levenshtein, { maxSize: 50 }); // Custom key generation for complex arguments const processUser = (user) => expensive(user); const memoizedProcess = memoize(processUser, { getKey: (user) => user.id, // Cache by user ID only }); // Real-world example: Fuzzy search with caching import { fuzzyMatch, memoize } from "nano-string-utils"; const cachedFuzzyMatch = memoize(fuzzyMatch); const searchResults = items.map((item) => cachedFuzzyMatch(query, item.name)); // Batch processing with deduplication benefits const words = ["hello", "world", "hello", "test", "world"]; const distances = words.map((word) => memoizedLevenshtein("example", word)); // Only computes 3 times instead of 5 ``` Features: - **LRU cache eviction** - Keeps most recently used results - **Configurable cache size** - Control memory usage (default: 100 entries) - **Custom key generation** - Support for complex argument types - **Type-safe** - Preserves function signatures and types - **Zero dependencies** - Pure JavaScript implementation Best used with: - `levenshtein()` - Expensive O(nΓ—m) algorithm - `fuzzyMatch()` - Complex scoring with boundary detection - `diff()` - Character-by-character comparison - Any custom expensive string operations Options: - `maxSize` - Maximum cached results (default: 100) - `getKey` - Custom cache key generator function </details> <details> <summary><b>πŸ”§ TypeScript Utilities</b></summary> ### Branded Types (TypeScript) Nano-string-utils provides branded types for compile-time type safety with validated strings. These types add zero runtime overhead and are fully tree-shakeable. ```typescript import { branded } from "nano-string-utils"; ``` #### Type Guards Type guards narrow string types to branded types: ```typescript const input: string = getUserInput(); if (branded.isValidEmail(input)) { // input is now typed as Email sendEmail(input); } if (branded.isValidUrl(input)) { // input is now typed as URL fetch(input); } if (branded.isSlug(input)) { // input is now typed as Slug useAsRoute(input); } ``` #### Builder Functions Safely create branded types with validation: ```typescript // Returns Email | null const email = branded.toEmail("user@example.com"); if (email) { sendEmail(email); // email is typed as Email } // Returns URL | null const url = branded.toUrl("https://example.com"); if (url) { fetch(url); // url is typed as URL } // Always returns Slug (transforms input) const slug = branded.toSlug("Hello World!"); // 'hello-world' as Slug createRoute(slug); // Smart slug handling const slug2 = branded.ensureSlug("already-a-slug"); // returns as-is if valid const slug3 = branded.ensureSlug("Not A Slug!"); // transforms to 'not-a-slug' ``` #### Assertion Functions Assert types with runtime validation: ```typescript const input: string = getUserInput(); // Throws BrandedTypeError if invalid branded.assertEmail(input); // input is now typed as Email sendEmail(input); // Custom error messages branded.assertUrl(input, "Invalid webhook URL"); // All assertion functions available branded.assertEmail(str); branded.assertUrl(str); branded.assertSlug(str); ``` #### Unsafe Variants For trusted inputs where validation isn't needed: ```typescript // Use only when you're certain the input is valid const trustedEmail = branded.unsafeEmail("admin@system.local"); const trustedUrl = branded.unsafeUrl("https://internal.api"); const trustedSlug = branded.unsafeSlug("already-valid-slug"); ``` #### Available Types - `Email` - Validated email addresses - `URL` - Validated URLs (http/https/ftp/ftps) - `Slug` - URL-safe slugs (lowercase, hyphenated) - `Brand<T, K>` - Generic branding utility for custom types #### Benefits - **Zero runtime overhead** - Types are erased at compilation - **Type safety** - Prevent passing unvalidated strings to functions - **IntelliSense support** - Full autocomplete and type hints - **Tree-shakeable** - Only imported if used - **Composable** - Works with existing string functions ```typescript // Example: Type-safe API function sendNewsletter(email: branded.Email) { // Can only be called with validated emails api.send(email); } // Won't compile without validation const userInput = "maybe@email.com"; // sendNewsletter(userInput); // ❌ Type error! // Must validate first const validated = branded.toEmail(userInput); if (validated) { sendNewsletter(validated); // βœ… Type safe! } ``` ### Template Literal Types (TypeScript) Case conversion functions now provide precise type inference for literal strings at compile time. This feature enhances IDE support with exact type transformations while maintaining full backward compatibility. ```typescript import { camelCase, kebabCase, snakeCase } from "nano-string-utils"; // Literal strings get exact transformed types const endpoint = kebabCase("getUserProfile"); // Type: "get-user-profile" (not just string!) const column = snakeCase("firstName"); // Type: "first_name" const methodName = camelCase("fetch-user-data"); // Type: "fetchUserData" // Runtime strings still return regular string type const userInput: string = getUserInput(); const result = camelCase(userInput); // Type: string (backward compatible) ``` #### All Case Conversions Support Template Literals ```typescript camelCase("hello-world"); // Type: "helloWorld" kebabCase("helloWorld"); // Type: "hello-world" snakeCase("HelloWorld"); // Type: "hello_world" pascalCase("hello-world"); // Type: "HelloWorld" constantCase("helloWorld"); // Type: "HELLO_WORLD" dotCase("HelloWorld"); // Type: "hello.world" pathCase("helloWorld"); // Type: "hello/world" sentenceCase("hello-world"); // Type: "Hello world" titleCase("hello-world"); // Type: "Hello World" ``` #### Type-Safe Configuration Objects Transform configuration keys between naming conventions: ```typescript const config = { "api-base-url": "https://api.example.com", "max-retries": 3, } as const; // Convert keys to camelCase at type level type ConfigCamelCase = { [K in keyof typeof config as CamelCase<K>]: (typeof config)[K]; }; // Type: { apiBaseUrl: string; maxRetries: number; } ``` #### API Route Mapping Create type-safe method names from API routes: ```typescript type ApiRoutes = "user-profile" | "user-settings" | "admin-panel"; type MethodNames = { [K in ApiRoutes as `fetch${PascalCase<K>}`]: () => Promise<void>; }; // Creates: fetchUserProfile(), fetchUserSettings(), fetchAdminPanel() ``` Benefits: - βœ… **Zero runtime cost** - All transformations happen at compile time - βœ… **Better IDE support** - Autocomplete shows exact transformed strings - βœ… **Type safety** - Catch typos and incorrect transformations during development - βœ… **Backward compatible** - Runtime strings work exactly as before </details> ### Null/Undefined Safety All functions in nano-string-utils handle null and undefined inputs gracefully: ```typescript // No more runtime errors! slugify(null); // Returns: null slugify(undefined); // Returns: undefined slugify(""); // Returns: "" // Consistent behavior across all functions isEmail(null); // Returns: false (validation functions) words(null); // Returns: [] (array functions) wordCount(null); // Returns: 0 (counting functions) // Safe to use without defensive checks const userInput = getUserInput(); // might be null/undefined const slug = slugify(userInput); // Won't throw! ``` This means: - βœ… **No TypeErrors** - Functions never throw on null/undefined - βœ… **Predictable behavior** - Consistent handling across all utilities - βœ… **Cleaner code** - No need for defensive checks before calling functions - βœ… **Zero performance cost** - Minimal overhead from null checks ## Bundle Size Each utility is optimized to be as small as possible: | Function | Size (minified) | | --------------------- | --------------- | | slugify | ~200 bytes | | camelCase | ~250 bytes | | snakeCase | ~220 bytes | | kebabCase | ~200 bytes | | pascalCase | ~180 bytes | | constantCase | ~230 bytes | | dotCase | ~210 bytes | | pathCase | ~210 bytes | | sentenceCase | ~280 bytes | | titleCase | ~320 bytes | | capitalize | ~100 bytes | | truncate | ~150 bytes | | stripHtml | ~120 bytes | | escapeHtml | ~180 bytes | | excerpt | ~220 bytes | | randomString | ~200 bytes | | hashString | ~150 bytes | | reverse | ~80 bytes | | deburr | ~200 bytes | | isEmail | ~180 bytes | | isUrl | ~200 bytes | | isASCII | ~100 bytes | | toASCII | ~450 bytes | | wordCount | ~100 bytes | | normalizeWhitespace | ~280 bytes | | removeNonPrintable | ~200 bytes | | template | ~350 bytes | | templateSafe | ~400 bytes | | pad | ~180 bytes | | padStart | ~150 bytes | | padEnd | ~150 bytes | | graphemes | ~250 bytes | | codePoints | ~120 bytes | | highlight | ~320 bytes | | diff | ~280 bytes | | levenshtein | ~380 bytes | | levenshteinNormalized | ~100 bytes | | fuzzyMatch | ~500 bytes | | pluralize | ~350 bytes | | singularize | ~320 bytes | | smartSplit | ~1.1KB | | humanizeList | ~850 bytes | | memoize | ~400 bytes | | extractEntities | ~1.1KB | Total package size: **< 7.5KB** minified + gzipped ## Requirements - Node.js >= 18 - TypeScript >= 5.0 (for TypeScript users) ## Development ```bash # Clone the repository git clone https://github.com/Zheruel/nano-string-utils.git cd nano-string-utils # Install dependencies npm install # Run tests npm test # Run tests with coverage npm run test:coverage # Build the library npm run build # Type check npm run typecheck ``` ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change. 1. Fork the repository 2. Create your feature branch (`git checkout -b feature/amazing-feature`) 3. Commit your changes (`git commit -m 'Add some amazing feature'`) 4. Push to the branch (`git push origin feature/amazing-feature`) 5. Open a Pull Request ## License MIT Β© [Zheruel] --- ## Why nano-string-utils? In a world of bloated dependencies, `nano-string-utils` stands out by providing exactly what you need and nothing more: - **Security First**: Zero dependencies means zero supply chain vulnerabilities - **Performance**: Optimized for both speed and size - 2.1-2.6x faster than lodash for truncate - 25x faster than lodash for template - 2.4x faster than lodash for capitalize - **Developer Experience**: Full TypeScript support with comprehensive JSDoc comments - **Production Ready**: 100% test coverage with extensive edge case handling - **Modern**: Built for ES2022+ with full ESM support and CommonJS compatibility ## Benchmarks We continuously benchmark nano-string-utils against popular alternatives (lodash and es-toolkit) to ensure optimal performance and bundle size. ### Running Benchmarks ```bash # Run all benchmarks npm run bench:all # Run performance benchmarks only npm run bench:perf # Run bundle size analysis only npm run bench:size ``` ### Latest Results #### Bundle Size Comparison (gzipped) | Function | nano-string-utils | lodash | es-toolkit | Winner | | ---------- | ----------------- | ------ | ---------- | ------- | | camelCase | 193B | 3.4KB | 269B | nano βœ… | | capitalize | 90B | 1.7KB | 99B | nano βœ… | | kebabCase | 161B | 2.8KB | 193B | nano βœ… | | truncate | 125B | 2.9KB | - | nano βœ… | [View full benchmark results](./benchmarks/BENCHMARK_RESULTS.md) ### Key Findings - πŸ† **Smallest bundle sizes**: nano-string-utils wins 10 out of 11 tested functions - ⚑ **Superior performance**: 2-25x faster than lodash for key operations - πŸ“Š **Detailed benchmarks**: See [benchmark-results.md](./benchmarks/benchmark-results.md) for full comparison - ⚑ **Optimized performance**: - **2.25x faster** than lodash for short string truncation - Case conversions improved by **30-40%** in latest optimizations - Truncate function improved by **97.6%** (42x faster!) - 🌳 **Superior tree-shaking**: Each function is independently importable with minimal overhead ## Comparison with Alternatives | Library | Bundle Size | Dependencies | Tree-shakeable | TypeScript | | ----------------- | ----------- | ------------ | --------------------- | ---------- | | nano-string-utils | < 7.5KB | 0 | βœ… | βœ… | | lodash | ~70KB | 0 | ⚠️ Requires lodash-es | βœ… | | underscore.string | ~20KB | 0 | ❌ | ❌ | | voca | ~30KB | 0 | ❌ | βœ… | ## Support - πŸ› [Report bugs](https://github.com/Zheruel/nano-string-utils/issues) - πŸ’‘ [Request features](https://github.com/Zheruel/nano-string-utils/issues) - πŸ“– [Read the docs](https://github.com/Zheruel/nano-string-utils#api-reference) - ⭐ [Star on GitHub](https://github.com/Zheruel/nano-string-utils)