@socketsecurity/lib
Version:
Core utilities and infrastructure for Socket.dev security tools
469 lines (468 loc) • 15.2 kB
TypeScript
/**
* @fileoverview String manipulation utilities including ANSI code handling.
* Provides string processing, prefix application, and terminal output utilities.
*/
import { ansiRegex, stripAnsi } from './ansi';
// Import get-east-asian-width from external wrapper.
// This library implements Unicode Standard Annex #11 (East Asian Width).
// https://www.unicode.org/reports/tr11/
// Re-export ANSI utilities for backward compatibility.
export { ansiRegex, stripAnsi };
// Type definitions
declare const BlankStringBrand: unique symbol;
export type BlankString = string & {
[BlankStringBrand]: true;
};
declare const EmptyStringBrand: unique symbol;
export type EmptyString = string & {
[EmptyStringBrand]: true;
};
// IMPORTANT: Do not use destructuring here - use direct assignment instead.
// tsgo has a bug that incorrectly transpiles destructured exports, resulting in
// `exports.SomeName = void 0;` which causes runtime errors.
// See: https://github.com/SocketDev/socket-packageurl-js/issues/3
export declare const fromCharCode: (...codes: number[]) => string;
export interface ApplyLinePrefixOptions {
/**
* The prefix to add to each line.
* @default ''
*/
prefix?: string | undefined;
}
/**
* Apply a prefix to each line of a string.
*
* Prepends the specified prefix to the beginning of each line in the input string.
* If the string contains newlines, the prefix is added after each newline as well.
* When no prefix is provided or prefix is empty, returns the original string unchanged.
*
* @param str - The string to add prefixes to
* @param options - Configuration options
* @returns The string with prefix applied to each line
*
* @example
* ```ts
* applyLinePrefix('hello\nworld', { prefix: '> ' })
* // Returns: '> hello\n> world'
*
* applyLinePrefix('single line', { prefix: ' ' })
* // Returns: ' single line'
*
* applyLinePrefix('no prefix')
* // Returns: 'no prefix'
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function applyLinePrefix(str: string, options?: ApplyLinePrefixOptions | undefined): string;
/**
* Convert a camelCase string to kebab-case.
*
* Transforms camelCase strings by converting uppercase letters to lowercase
* and inserting hyphens before uppercase sequences. Handles consecutive
* uppercase letters (like "XMLHttpRequest") by treating them as a single word.
* Returns empty string for empty input.
*
* Note: This function only handles camelCase. For mixed formats including
* snake_case, use `toKebabCase()` instead.
*
* @param str - The camelCase string to convert
* @returns The kebab-case string
*
* @example
* ```ts
* camelToKebab('helloWorld')
* // Returns: 'hello-world'
*
* camelToKebab('XMLHttpRequest')
* // Returns: 'xmlhttprequest'
*
* camelToKebab('iOS')
* // Returns: 'ios'
*
* camelToKebab('')
* // Returns: ''
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function camelToKebab(str: string): string;
export interface IndentStringOptions {
/**
* Number of spaces to indent each line.
* @default 1
*/
count?: number | undefined;
}
/**
* Indent each line of a string with spaces.
*
* Adds the specified number of spaces to the beginning of each non-empty line
* in the input string. Empty lines (containing only whitespace) are not indented.
* Uses a regular expression to efficiently handle multi-line strings.
*
* @param str - The string to indent
* @param options - Configuration options
* @returns The indented string
*
* @example
* ```ts
* indentString('hello\nworld', { count: 2 })
* // Returns: ' hello\n world'
*
* indentString('line1\n\nline3', { count: 4 })
* // Returns: ' line1\n\n line3'
*
* indentString('single line')
* // Returns: ' single line' (default: 1 space)
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function indentString(str: string, options?: IndentStringOptions | undefined): string;
/**
* Check if a value is a blank string (empty or only whitespace).
*
* A blank string is defined as a string that is either:
* - Completely empty (length 0)
* - Contains only whitespace characters (spaces, tabs, newlines, etc.)
*
* This is useful for validation when you need to ensure user input
* contains actual content, not just whitespace.
*
* @param value - The value to check
* @returns `true` if the value is a blank string, `false` otherwise
*
* @example
* ```ts
* isBlankString('')
* // Returns: true
*
* isBlankString(' ')
* // Returns: true
*
* isBlankString('\n\t ')
* // Returns: true
*
* isBlankString('hello')
* // Returns: false
*
* isBlankString(null)
* // Returns: false
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function isBlankString(value: unknown): value is BlankString;
/**
* Check if a value is a non-empty string.
*
* Returns `true` only if the value is a string with at least one character.
* This includes strings containing only whitespace (use `isBlankString()` if
* you want to exclude those). Type guard ensures TypeScript knows the value
* is a string after this check.
*
* @param value - The value to check
* @returns `true` if the value is a non-empty string, `false` otherwise
*
* @example
* ```ts
* isNonEmptyString('hello')
* // Returns: true
*
* isNonEmptyString(' ')
* // Returns: true (contains whitespace)
*
* isNonEmptyString('')
* // Returns: false
*
* isNonEmptyString(null)
* // Returns: false
*
* isNonEmptyString(123)
* // Returns: false
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function isNonEmptyString(value: unknown): value is Exclude<string, EmptyString>;
export interface SearchOptions {
/**
* The position in the string to begin searching from.
* Negative values count back from the end of the string.
* @default 0
*/
fromIndex?: number | undefined;
}
/**
* Search for a regular expression in a string starting from an index.
*
* Similar to `String.prototype.search()` but allows specifying a starting
* position. Returns the index of the first match at or after `fromIndex`,
* or -1 if no match is found. Negative `fromIndex` values count back from
* the end of the string.
*
* This is more efficient than using `str.slice(fromIndex).search()` when
* you need the absolute position in the original string, as it handles
* the offset calculation for you.
*
* @param str - The string to search in
* @param regexp - The regular expression to search for
* @param options - Configuration options
* @returns The index of the first match, or -1 if not found
*
* @example
* ```ts
* search('hello world hello', /hello/, { fromIndex: 0 })
* // Returns: 0 (first 'hello')
*
* search('hello world hello', /hello/, { fromIndex: 6 })
* // Returns: 12 (second 'hello')
*
* search('hello world', /goodbye/, { fromIndex: 0 })
* // Returns: -1 (not found)
*
* search('hello world', /hello/, { fromIndex: -5 })
* // Returns: -1 (starts searching from 'world', no match)
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function search(str: string, regexp: RegExp, options?: SearchOptions | undefined): number;
/**
* Strip the Byte Order Mark (BOM) from the beginning of a string.
*
* The BOM (U+FEFF) is a Unicode character that can appear at the start of
* a text file to indicate byte order and encoding. In UTF-16 (JavaScript's
* internal string representation), it appears as 0xFEFF. This function
* removes it if present, leaving the rest of the string unchanged.
*
* Most text processing doesn't need to handle the BOM explicitly, but it
* can cause issues when parsing JSON, CSV, or other structured data formats
* that don't expect a leading invisible character.
*
* @param str - The string to strip BOM from
* @returns The string without BOM
*
* @example
* ```ts
* stripBom('\uFEFFhello world')
* // Returns: 'hello world'
*
* stripBom('hello world')
* // Returns: 'hello world' (no BOM to strip)
*
* stripBom('')
* // Returns: ''
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function stripBom(str: string): string;
/**
* Get the visual width of a string in terminal columns.
*
* Calculates how many columns a string will occupy when displayed in a terminal,
* accounting for:
* - ANSI escape codes (stripped before calculation)
* - Wide characters (CJK ideographs, fullwidth forms) that take 2 columns
* - Emoji (including complex sequences) that take 2 columns
* - Combining marks and zero-width characters (take 0 columns)
* - East Asian Width properties (Fullwidth, Wide, Halfwidth, Narrow, etc.)
*
* Based on string-width by Sindre Sorhus:
* https://socket.dev/npm/package/string-width/overview/7.2.0
* MIT License
* Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
*
* Terminal emulators display characters in a grid of cells (columns).
* Most ASCII characters take 1 column, but some characters (especially
* emoji and CJK characters) take 2 columns. This function calculates
* the actual visual width, which is crucial for:
* - Aligning text properly in tables or columns
* - Preventing text from jumping when characters change
* - Calculating padding/spacing for spinners and progress bars
* - Wrapping text at the correct column width
*
* Algorithm Overview:
* 1. Strip ANSI escape codes (invisible in terminal)
* 2. Segment into grapheme clusters (user-perceived characters)
* 3. For each cluster:
* - Skip zero-width/non-printing clusters (width = 0)
* - RGI emoji clusters are double-width (width = 2)
* - Otherwise use East Asian Width of first visible code point
* - Add width for trailing Halfwidth/Fullwidth Forms
*
* East Asian Width Categories (Unicode Standard Annex #11):
* - F (Fullwidth): 2 columns - e.g., fullwidth Latin letters (A, B)
* - W (Wide): 2 columns - e.g., CJK ideographs (漢字), emoji (⚡, 😀)
* - H (Halfwidth): 1 column - e.g., halfwidth Katakana (ア, イ)
* - Na (Narrow): 1 column - e.g., ASCII (a-z, 0-9)
* - A (Ambiguous): Context-dependent, treated as 1 column by default
* - N (Neutral): 1 column - e.g., most symbols (✦, ✧, ⋆)
*
* Why This Matters for Socket:
* - Lightning bolt (⚡) takes 2 columns
* - Stars (✦, ✧, ⋆) take 1 column
* - Without proper width calculation, spinner text jumps between frames
* - This function enables consistent alignment by calculating padding
*
* @param text - The string to measure
* @returns The visual width in terminal columns
*
* @example
* ```ts
* stringWidth('hello')
* // Returns: 5 (5 ASCII chars = 5 columns)
*
* stringWidth('⚡')
* // Returns: 2 (lightning bolt is wide)
*
* stringWidth('✦')
* // Returns: 1 (star is narrow)
*
* stringWidth('漢字')
* // Returns: 4 (2 CJK characters × 2 columns each)
*
* stringWidth('\x1b[31mred\x1b[0m')
* // Returns: 3 (ANSI codes stripped, 'red' = 3)
*
* stringWidth('👍🏽')
* // Returns: 2 (emoji with skin tone = 1 grapheme cluster = 2 columns)
*
* stringWidth('é')
* // Returns: 1 (combining accent doesn't add width)
*
* stringWidth('')
* // Returns: 0
* ```
*
* @throws {TypeError} When input is not a string
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function stringWidth(text: string): number;
/**
* Convert a string to kebab-case (handles camelCase and snake_case).
*
* Transforms strings from camelCase or snake_case to kebab-case by:
* - Converting uppercase letters to lowercase
* - Inserting hyphens before uppercase letters (for camelCase)
* - Replacing underscores with hyphens (for snake_case)
*
* This is more comprehensive than `camelToKebab()` as it handles mixed
* formats including snake_case. Returns empty string for empty input.
*
* @param str - The string to convert
* @returns The kebab-case string
*
* @example
* ```ts
* toKebabCase('helloWorld')
* // Returns: 'hello-world'
*
* toKebabCase('hello_world')
* // Returns: 'hello-world'
*
* toKebabCase('XMLHttpRequest')
* // Returns: 'xmlhttp-request'
*
* toKebabCase('iOS_Version')
* // Returns: 'io-s-version'
*
* toKebabCase('')
* // Returns: ''
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function toKebabCase(str: string): string;
/**
* Trim newlines from the beginning and end of a string.
*
* Removes all leading and trailing newline characters (both `\n` and `\r`)
* from a string, while preserving any newlines in the middle. This is similar
* to `String.prototype.trim()` but specifically targets newlines instead of
* all whitespace.
*
* Optimized for performance by checking the first and last characters before
* doing any string manipulation. Returns the original string unchanged if no
* newlines are found at the edges.
*
* @param str - The string to trim
* @returns The string with leading and trailing newlines removed
*
* @example
* ```ts
* trimNewlines('\n\nhello\n\n')
* // Returns: 'hello'
*
* trimNewlines('\r\nworld\r\n')
* // Returns: 'world'
*
* trimNewlines('hello\nworld')
* // Returns: 'hello\nworld' (middle newline preserved)
*
* trimNewlines(' hello ')
* // Returns: ' hello ' (spaces not trimmed, only newlines)
*
* trimNewlines('hello')
* // Returns: 'hello'
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function trimNewlines(str: string): string;
/**
* Repeat a string n times.
*
* Creates a new string by repeating the input string the specified number of times.
* Returns an empty string if count is zero or negative. This is a simple wrapper
* around `String.prototype.repeat()` with guard for non-positive counts.
*
* @param str - The string to repeat
* @param count - The number of times to repeat the string
* @returns The repeated string, or empty string if count <= 0
*
* @example
* ```ts
* repeatString('hello', 3)
* // Returns: 'hellohellohello'
*
* repeatString('x', 5)
* // Returns: 'xxxxx'
*
* repeatString('hello', 0)
* // Returns: ''
*
* repeatString('hello', -1)
* // Returns: ''
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function repeatString(str: string, count: number): string;
/**
* Center text within a given width.
*
* Adds spaces before and after the text to center it within the specified width.
* Distributes padding evenly on both sides. When the padding is odd, the extra
* space is added to the right side. Strips ANSI codes before calculating text
* length to ensure accurate centering of colored text.
*
* If the text is already wider than or equal to the target width, returns the
* original text unchanged (no truncation occurs).
*
* @param text - The text to center (may include ANSI codes)
* @param width - The target width in columns
* @returns The centered text with padding
*
* @example
* ```ts
* centerText('hello', 11)
* // Returns: ' hello ' (3 spaces on each side)
*
* centerText('hi', 10)
* // Returns: ' hi ' (4 spaces on each side)
*
* centerText('odd', 8)
* // Returns: ' odd ' (2 left, 3 right)
*
* centerText('\x1b[31mred\x1b[0m', 7)
* // Returns: ' \x1b[31mred\x1b[0m ' (ANSI codes preserved, 'red' centered)
*
* centerText('too long text', 5)
* // Returns: 'too long text' (no truncation, returned as-is)
* ```
*/
/*@__NO_SIDE_EFFECTS__*/
export declare function centerText(text: string, width: number): string;