Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions .github/workflows/test-on-pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,21 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm' # Added caching for faster PR checks

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build
- name: Run tests
run: npm test

- name: Run Unit Tests
run: npm run test:unit

- name: Run Prototype Integration Tests
run: npm run test:integration
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": " npm run build && node --test",
"test": "npm run build && node --test dist/tests",
"test:unit": "node --test dist/tests/transformations dist/tests/validations dist/tests/formatting dist/tests/analyzing",
"test:integration": "node --test dist/tests/integration",
"prepublishOnly": "npm run build",
"format": "prettier --write ."
},
Expand Down
6 changes: 5 additions & 1 deletion src/analyzing/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import { patternCount } from './patternCount';
import { vowelConsonantCount } from './vowelConsonantCount';
import { checkMultiplePatterns } from './checkMultiplePatterns';
import { checkSubsequence } from './checkSubsequence';
import { functionWordCount } from './functionWordCount';
import { contentWordCount } from './contentWordCount';
import { checkStringRotations } from './stringRotation';
import { lexicographicalRank } from './lexicographicalRank';

Expand All @@ -37,6 +39,8 @@ export const analyzing = {
vowelConsonantCount,
checkMultiplePatterns,
checkSubsequence,
functionWordCount,
contentWordCount,
checkStringRotations,
lexicographicalRank
lexicographicalRank,
};
151 changes: 147 additions & 4 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,157 @@ export * from './formatting';
export * from './transformations';
export * from './validations';

import { analyzing } from './analyzing';
import { formatting } from './formatting';
import { transformations } from './transformations';
import { validations } from './validations';
import * as analyzing from './analyzing';
import * as formatting from './formatting';
import * as transformations from './transformations';
import * as validations from './validations';

declare global {
interface String {
/* =========================
Transformations
========================= */

truncateText(maxLength: number, suffix?: string): string;
toSlug(): string;
capitalizeWords(): string;
removeSpecialChars(): string;
removeWords(words: string[]): string;
removeDuplicates(): string;
initials(): string;

camelCase(): string;
pascalCase(): string;
snakeCase(): string;
kebabCase(): string;
titleCase(): string;
constantCase(): string;

escapeHTML(): string;
maskSegment(start: number, end: number, maskChar?: string): string;
deburr(): string;
splitChunks(chunkSize: number): string[];

numberToText(lang?: string): string;
reverseWordsInString(): string;

stringPermutations(): string[];
stringPermutationsGenerator(): Generator<string>;
stringCombinations(): string[];

/* =========================
Validations
========================= */

isURL(): boolean;
isEmail(): boolean;
isDate(): boolean;
isEmpty(): boolean;
isSlug(): boolean;
isTypeOf(type: string): boolean;

isIPv4(): boolean;
isIPv6(): boolean;
isHexColor(): boolean;

isPalindrome(): boolean;
isCoordinates(): boolean;

isLowerCase(): boolean;
isUpperCase(): boolean;
isAlphabetic(): boolean;
isAlphaNumeric(): boolean;

isAnagram(other: string): boolean;
isMacAddress(): boolean;
isPanagram(): boolean;

/* =========================
Analysis
========================= */

wordCount(): number;
contentWordCount(): number;
functionWordCount(): number;
readingDuration(wordsPerMinute?: number): number;

characterCount(): number;
characterFrequency(): Record<string, number>;

stringSimilarity(other: string): number;
complexity(): {
score: number;
uniqueness: number;
length: number;
};

patternCount(pattern: string | RegExp): number;
vowelConsonantCount(): {
vowels: number;
consonants: number;
};

checkMultiplePatterns(patterns: string[]): Record<string, number>;
checkSubsequence(subsequence: string): boolean;
stringRotation(other: string): boolean;

lexicographicalRank(): number;

/* =========================
Formatting
========================= */

capitalize(): string;
formatNumber(locale?: string): string;
formatPhone(countryCode?: string): string;

formatDuration(): string;
trim(): string;

formatRomanNumeral(): string;
formatPercentage(decimals?: number): string;
formatFileSize(): string;

formatOrdinal(): string;
formatList(conjunction?: string): string;

formatCreditCard(): string;

formatToOctal(prefix?: boolean): string;
formatTemperature(from: 'C' | 'F' | 'K', to: 'C' | 'F' | 'K'): string;

formatScientific(precision?: number): string;
formatToBinary(groupBits?: boolean): string;
formatToHexadecimal(prefix?: boolean): string;
formatToDecimal(base: 2 | 8 | 16): number;
}
}

export function extendStringPrototype(): void {
const modules = [analyzing, formatting, transformations, validations];

modules.forEach((module) => {
Object.keys(module).forEach((key) => {
const fn = (module as any)[key];

if (typeof fn === 'function' && !String.prototype.hasOwnProperty(key)) {
Object.defineProperty(String.prototype, key, {
value: function (this: string, ...args: any[]) {
return fn(this, ...args);
},
writable: true,
configurable: true,
enumerable: false,
});
}
});
});
}

export default {
analyzing,
formatting,
transformations,
validations,
extendStringPrototype,
};
30 changes: 16 additions & 14 deletions src/tests/analyzing/contentWordCount.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import test from "node:test";
import assert from "node:assert/strict";
import { contentWordCount } from "../../analyzing/contentWordCount.js";
import test, { describe } from 'node:test';
import assert from 'node:assert/strict';
import { contentWordCount } from '../../analyzing/contentWordCount.js';

test("counts content words in a normal sentence", () => {
assert.equal(contentWordCount("This is a test of the system"), 2);
});
describe('contentWordCount', () => {
test('counts content words in a normal sentence', () => {
assert.equal(contentWordCount('This is a test of the system'), 2);
});

test("returns 0 when there are no content words", () => {
assert.equal(contentWordCount("is the at of"), 0);
});
test('returns 0 when there are no content words', () => {
assert.equal(contentWordCount('is the at of'), 0);
});

test("ignores case and punctuation", () => {
assert.equal(contentWordCount("Elephants, ELEPHANTS, elephants!"), 3);
});
test('ignores case and punctuation', () => {
assert.equal(contentWordCount('Elephants, ELEPHANTS, elephants!'), 3);
});

test("returns 0 for empty string", () => {
assert.equal(contentWordCount(""), 0);
test('returns 0 for empty string', () => {
assert.equal(contentWordCount(''), 0);
});
});
30 changes: 16 additions & 14 deletions src/tests/analyzing/functionWordCount.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import test from "node:test";
import assert from "node:assert/strict";
import { functionWordCount } from "../../analyzing/functionWordCount.js";
import test, { describe } from 'node:test';
import assert from 'node:assert/strict';
import { functionWordCount } from '../../analyzing/functionWordCount.js';

test("counts function words in a normal sentence", () => {
assert.equal(functionWordCount("This is a test of the system"), 5);
});
describe('functionWordCount', () => {
test('counts function words in a normal sentence', () => {
assert.equal(functionWordCount('This is a test of the system'), 5);
});

test("returns 0 when there are no function words", () => {
assert.equal(functionWordCount("Elephants run fast"), 0);
});
test('returns 0 when there are no function words', () => {
assert.equal(functionWordCount('Elephants run fast'), 0);
});

test("ignores case and punctuation", () => {
assert.equal(functionWordCount("The, THE, the!"), 3);
});
test('ignores case and punctuation', () => {
assert.equal(functionWordCount('The, THE, the!'), 3);
});

test("returns 0 for empty string", () => {
assert.equal(functionWordCount(""), 0);
test('returns 0 for empty string', () => {
assert.equal(functionWordCount(''), 0);
});
});
46 changes: 46 additions & 0 deletions src/tests/integration/prototype.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { describe, it, before } from 'node:test';
import assert from 'node:assert';
// Adjust the path to reach src/index.ts from src/tests/integration/
import {
extendStringPrototype,
transformations,
validations,
analyzing,
formatting,
} from '../../index';

describe('Global String Prototype Extension', () => {
before(() => {
extendStringPrototype();
});

const categories = [
{ name: 'Transformations', module: transformations },
{ name: 'Validations', module: validations },
{ name: 'Analyzing', module: analyzing },
{ name: 'Formatting', module: formatting },
];

categories.forEach(({ name, module }) => {
describe(`${name} module`, () => {
Object.keys(module).forEach((fnName) => {
it(`should have ${fnName} attached to String.prototype`, () => {
assert.strictEqual(typeof (String.prototype as any)[fnName], 'function');
});
});
});
});

it('should ensure methods are non-enumerable', () => {
const str = 'test';
const keys: string[] = [];

// Using Object(str) to avoid the TS2407 error you encountered earlier
for (const key in Object(str)) {
keys.push(key);
}

// Verify that a common method like 'camelCase' isn't leaking into loops
assert.strictEqual(keys.includes('camelCase'), false);
});
});
46 changes: 25 additions & 21 deletions src/transformations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export { toSlug } from './toSlug';
export { truncateText } from './truncateText';
export { escapeHtml } from './escapeHTML';
export { maskSegment } from './maskSegment';
export { deburr } from './deburr';
export { splitChunks } from './splitChunks';
export { numberToText } from './numberToText/main';
export { reverseWordsInString } from './reverseWordsInString ';
export { stringPermutations, stringPermutationsGenerator } from './stringPermutations';
Expand All @@ -34,31 +36,33 @@ import { truncateText } from './truncateText';
import { escapeHtml } from './escapeHTML';
import { maskSegment } from './maskSegment';
import { deburr } from './deburr';
import { splitChunks } from './splitChunks';
import { numberToText } from './numberToText/main';
import { reverseWordsInString } from './reverseWordsInString ';
import { stringPermutations, stringPermutationsGenerator } from './stringPermutations';
import { stringCombinations } from './stringCombinations';

export const transformations = {
camelCase,
capitalizeWords,
constantCase,
initials,
kebabCase,
pascalCase,
removeDuplicates,
removeSpecialChars,
removeWords,
snakeCase,
titleCase,
toSlug,
truncateText,
escapeHtml,
maskSegment,
deburr,
numberToText,
reverseWordsInString,
stringPermutations,
stringPermutationsGenerator,
stringCombinations
camelCase,
capitalizeWords,
constantCase,
initials,
kebabCase,
pascalCase,
removeDuplicates,
removeSpecialChars,
removeWords,
snakeCase,
titleCase,
toSlug,
truncateText,
escapeHtml,
maskSegment,
deburr,
splitChunks,
numberToText,
reverseWordsInString,
stringPermutations,
stringPermutationsGenerator,
stringCombinations,
};
Loading