diff --git a/README.md b/README.md
index ca80cb0..80a8d44 100644
--- a/README.md
+++ b/README.md
@@ -92,6 +92,7 @@ These changes improve throughput and reduce memory pressure when working with la
- [stringPermutations](#stringpermutations) - Generates all unique permutations of a given string.
- [stringPermutationsGenerator](#stringpermutationsgenerator) - Generator-based permutations API for lazy iteration.
- [stringCombinations](#stringcombinations) - Generates all unique combinations of a given string.
+- [pipeLine](#pipeline) - Can be used to chain multiple transformations, and powerful tools to manipulate data flow
### Validations
@@ -473,6 +474,146 @@ deburr('über cool');
| --------- | ------ | -------- | ----------------------------------------- |
| text | string | required | The input string to strip diacritics from |
+#### `pipeLine(args)`
+Apply multiple transformations in any order and add effects that detect
+changes during transformation process.
+
+```typescript
+pipeLine({
+ initial: "Hello World!",
+ pipes: [ camelCase, capitalize ]
+}).output; // Helloworld
+```
+Optionally, an effect callback can be added to have more effects in-between transformations.
+
+```typescript
+pipeLine({
+ initial: "Evil Code Exceeding So Much Space!",
+ pipe: [camelCase, removeDuplicates],
+ effect({ target, restartBeforeTransform }) {
+
+ if (target.length > 10) {
+ //evil code
+ restartBeforeTransform(target.slice(0, -1))
+ }
+
+ }
+}).output // evilcodeex
+```
+
+#### `pipeLine` Parameters
+
+| Parameter | Type | Default | Description |
+|------------|----------------------------------------|-----------|-----------------------------------------------------------------------------|
+| `initial` | `string` | required | The initial input string to be processed. |
+| `pipe` | `Array<(data: string) => string>` | required | Transformation functions applied sequentially to the input string. |
+| `effect` | `(args: EffectArgs) => any` | optional | Optional effect function with access to transformation context and control. |
+
+#### `effect` Function Context (`EffectArgs`)
+
+| Property | Type | Description |
+|------------------------|---------------------------------------------------|-----------------------------------------------------------------------------|
+| `history` | `Array<{ operation: string, value: string }>` | A log of each transformation step and its result. |
+| `first` | `string` | The original string before any transformations. |
+| `target` | `string` | The final string after all transformations are complete. |
+| `pipe` | `Array<(data: string) => string>` | The original array of transformation functions. |
+| `abort` | `() => any` | Stops pipeline execution immediately. |
+| `forceStop` | `(last: string) => any` | Halts processing and returns the current result. |
+| `next` | `(string: string) => any` | Continues pipeline execution from the next transformation. |
+| `restartBeforeTransform` | `(string?: string) => any` | Restarts the pipeline before calling the effect; optionally with new input.|
+| `restartAfterTransform` | `(string?: string) => any` | Restarts the pipeline after calling the effect; optionally with new input. |
+| `moveToPipeIndex` | `(index: number) => any` | Jumps to a specific transformation index to resume processing. |
+
+#### `pipe(initial)`
+
+Lightweight and framework-agnostic string transformer with optional effect hooks and full pipeline control. Chain `.flow()` to get output or `.raw()` for transformation metadata.
+
+```ts
+pipe("Evil Code Exceeding So Much Space!")
+ .effect(({ target, restartBeforeTransform }) => {
+
+ if (target.length > 10) {
+ restartBeforeTransform(target.slice(0, -1))
+ }
+
+ })
+ .flow(camelCase, removeDuplicates) // evilcodeex
+```
+
+| Parameter | Type | Default | Description |
+|-----------|--------|----------|--------------------------------------|
+| initial | string | required | The input string to be transformed. |
+
+---
+
+#### `pipe().flow(...pipe)`
+
+Applies all transformation functions in order and returns the final output string.
+
+```ts
+pipe("quick brown fox").flow(trim, camelCase); // quickBrownFox
+```
+
+| Parameter | Type | Default | Description |
+|-----------|----------------------------------------|----------|------------------------------------------------|
+| pipe | `((data: string) => string)[]` | required | List of transformation functions to apply. |
+
+---
+
+#### `pipe().effect(callback)`
+
+Registers an effect hook that runs after transformation with full context and pipeline controls.
+
+```ts
+pipe("too long string!!")
+ .effect(({ target, restartBeforeTransform }) => {
+ if (target.length > 10) {
+ restartBeforeTransform(target.slice(0, 10));
+ }
+ })
+ .flow(removeSymbols); // toolongstr
+```
+
+| Parameter | Type | Default | Description |
+|-----------|----------------|----------|----------------------------------------------|
+| callback | `EffectArgs` | required | Hook with full access to pipeline control. |
+
+---
+
+#### `pipe().raw(...pipe)`
+
+Same as `.flow()`, but returns a full result object including the transformation history and utilities.
+
+```ts
+const result = pipe("hello evil evil code")
+ .effect(({ history }) => console.log(history))
+ .raw(removeDuplicates);
+
+console.log(result.output); // helloEvilCode
+```
+
+| Parameter | Type | Default | Description |
+|-----------|----------------------------------------|----------|----------------------------------------------|
+| pipe | `((data: string) => string)[]` | required | List of transformation functions to apply. |
+
+---
+
+#### `EffectArgs` (effect context)
+
+| Property | Type | Description |
+|--------------------------|-------------------------------------------------|--------------------------------------------------------------------------|
+| `history` | `Array<{ operation: string, value: string }>` | Log of each transformation step and its result. |
+| `first` | `string` | Original string before any transformations. |
+| `target` | `string` | Final result after transformations. |
+| `pipe` | `((data: string) => string)[]` | The transformation pipeline. |
+| `abort` | `() => void` | Stop pipeline execution immediately. |
+| `forceStop` | `(last: string) => void` | Stop execution and return a specific value. |
+| `next` | `(newString: string) => void` | Continue to the next step with a different string. |
+| `restartBeforeTransform` | `(newInitial?: string) => void` | Restart pipeline from beginning before calling the effect. |
+| `restartAfterTransform` | `(newInitial?: string) => void` | Restart pipeline after the effect has been called. |
+| `moveToPipeIndex` | `(index: number) => void` | Jump to a specific transformation step and continue. |
+
+
---
#### `splitChunks(text, chunkSize)`
diff --git a/src/index.ts b/src/index.ts
index c8295bf..37a20d4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -13,4 +13,4 @@ export default {
formatting,
transformations,
validations,
-};
+};
\ No newline at end of file
diff --git a/src/tests/transformations/pipeLine.test.ts b/src/tests/transformations/pipeLine.test.ts
new file mode 100644
index 0000000..670ce74
--- /dev/null
+++ b/src/tests/transformations/pipeLine.test.ts
@@ -0,0 +1,138 @@
+import { describe, it } from 'node:test';
+import assert, { match } from 'node:assert';
+import { pipe, pipeLine } from "../../transformations/pipe";
+import { capitalize } from '../../formatting';
+import { camelCase, removeDuplicates, truncateText } from '../../transformations';
+
+describe('pipeLine', () => {
+
+ it('can run piped function', () => {
+ assert.strictEqual(
+ pipeLine({
+ initial: "hello world world",
+ pipe: [capitalize, removeDuplicates, camelCase]
+ }).output, "helloWorld"
+ )
+ });
+
+ it('can abort', () => {
+
+ assert.strictEqual(
+ pipeLine({
+
+ initial: "Phone Number: 123456",
+
+ pipe: [
+ capitalize, removeDuplicates, camelCase /*should abort here*/, (text) => truncateText(text, 5)
+ ],
+
+ effect({ abort, target }) {
+ if (target == "phoneNumber123456") abort()
+ }
+ }
+
+ ).output, "phoneNumber123456"
+
+ );
+ })
+
+ it('can restart', () => {
+ assert.strictEqual(
+ pipeLine({
+
+ initial: "123456",
+
+ pipe: [
+ (text) => text.slice(0, -1)
+ ],
+
+ effect({ restartAfterTransform, target }) {
+ if (target != "") restartAfterTransform()
+ }
+
+ }).output, ""
+ )
+ });
+
+ it('can force stop', () => {
+
+ assert.strictEqual(
+ pipeLine({
+
+ initial: "I ate the the apple",
+
+ pipe: [
+ removeDuplicates, (text) => text
+ ],
+
+ //called after every transformation
+ effect({ history, forceStop, target }) {
+ if (target.includes('apple') && history.length > 1) {
+ forceStop(target.replace('apple', '*****'))
+ }
+ }
+
+ }).output
+ , "I ate the *****"
+ )
+
+ });
+
+ it('testing restart before', () => {
+
+ assert.strictEqual(
+
+ pipeLine({
+ initial: "Evil Code Exceeding So Much Space!",
+ pipe: [camelCase, removeDuplicates],
+ effect({ target, restartBeforeTransform }) {
+
+ if (target.length > 10) {
+ //evil code
+ restartBeforeTransform(target.slice(0, -1))
+ }
+
+ }
+ }).output, "evilcodeex"
+ )
+
+ });
+
+ it('simple pipe test', () => {
+
+ assert.strictEqual(
+ pipe("Hello World").flow(camelCase), "helloWorld"
+ )
+
+ });
+
+ it('testing restart before using simple pipes', () => {
+
+ assert.strictEqual(
+ pipe("Evil Code Exceeding So Much Space!")
+ .effect(({ target, restartBeforeTransform }) => {
+
+ if (target.length > 10) {
+ restartBeforeTransform(target.slice(0, -1))
+ }
+
+ })
+ .flow(camelCase, removeDuplicates),
+
+ "evilcodeex"
+ )
+
+ });
+
+ it('reuseable pipes', () => {
+
+ const profanityFilter = (input: string) => pipe(input).flow(removeDuplicates, camelCase, (text) => text.replace('apple', '*****').replace("Apple", '*****'))
+
+ assert.strictEqual(
+ //seems like there's an issue with question mark?
+ profanityFilter("Who the apple are you?"), "whoThe*****AreYou "
+ )
+
+ })
+
+})
\ No newline at end of file
diff --git a/src/transformations/index.ts b/src/transformations/index.ts
index 187d8db..805b94d 100644
--- a/src/transformations/index.ts
+++ b/src/transformations/index.ts
@@ -38,6 +38,7 @@ import { numberToText } from './numberToText/main';
import { reverseWordsInString } from './reverseWordsInString ';
import { stringPermutations, stringPermutationsGenerator } from './stringPermutations';
import { stringCombinations } from './stringCombinations';
+import { pipe, pipeLine } from './pipe';
export const transformations = {
camelCase,
@@ -60,5 +61,7 @@ export const transformations = {
reverseWordsInString,
stringPermutations,
stringPermutationsGenerator,
- stringCombinations
+ stringCombinations,
+ pipeLine,
+ pipe
};
diff --git a/src/transformations/pipe.ts b/src/transformations/pipe.ts
new file mode 100644
index 0000000..9972e7d
--- /dev/null
+++ b/src/transformations/pipe.ts
@@ -0,0 +1,200 @@
+//TYPE DEFINITIONS ARE STORED HERE.
+
+export type EffectArgs = (args: {
+ history: {
+ operation: string,
+ value: string
+ }[],
+ first: string,
+ target: string,
+ pipe: ((data: string) => string)[],
+ abort: () => any,
+ forceStop: (last: string) => any,
+ next: (string: string) => any,
+ restartBeforeTransform: (string?: string) => any,
+ restartAfterTransform: (string?: string) => any,
+ moveToPipeIndex: (index: number) => any
+}) => any
+
+/**
+ * Processes a string through a pipeline of transformation functions, optionally invoking an effect
+ * hook with full context and control utilities.
+ *
+ * I have made sure to make it work on any framework; i.e framework agonistic function, can be
+ * used in react/svelte/vue or anything for that matter.
+ *
+ * @author github.com/MayukhChakrabortyDX
+ * @param {Object} params - Configuration object.
+ * @param {string} params.initial - The initial input string to be processed.
+ * @param {Array<(data: string) => string>} params.pipe - An array of transformation functions to be applied sequentially.
+ * @param {(args: {
+ * history: { operation: string, value: string }[],
+ * first: string,
+ * target: string,
+ * pipe: Array<(data: string) => string>,
+ * abort: () => any,
+ * forceStop: (last: string) => any,
+ * next: (string: string) => any,
+ * restartBeforeTransform: (string?: string) => any,
+ * restartAfterTransform: (string?: string) => any,
+ * moveToPipeIndex: (index: number) => any
+ * }) => any} [params.effect] - Optional effect function that receives full control over the pipeline execution.
+ *
+ * @property {Array<{ operation: string, value: string }>} history - A log of each transformation step applied.
+ * @property {string} first - The original input string before any transformations.
+ * @property {string} target - The final string after all transformations are complete.
+ * @property {Array<(data: string) => string>} pipe - The array of transformation functions.
+ * @property {Function} abort - Stops execution immediately.
+ * @property {Function} forceStop - Halts execution and returns the last computed value.
+ * @property {Function} next - Continues processing from the next step with a given string.
+ * @property {Function} restartBeforeTransform - Re-runs the entire pipeline starting from the first transform. Optionally accepts a new initial string.
+ * @property {Function} restartAfterTransform - Re-runs the pipeline after effect is invoked. Optionally accepts a new string.
+ * @property {Function} moveToPipeIndex - Jumps to a specific index in the pipeline to resume processing.
+ */
+
+export function pipeLine(
+ { initial, pipe, effect = () => { } }: {
+ initial: string,
+ pipe: ((data: string) => string)[],
+ effect?: EffectArgs
+ }
+) {
+ const cacheStart = initial;
+ let abortSignal = false
+ let continueSignal = false
+ let restartAfterTransform = false
+ const history: {
+ operation: string,
+ value: string
+ }[] = [{
+ operation: "pipe", value: cacheStart
+ }]
+
+ for (let pipeIndex = 0; pipeIndex < pipe.length; pipeIndex++) {
+
+ continueSignal = false
+ restartAfterTransform = false
+
+ effect(
+ {
+ history,
+ first: cacheStart,
+ target: initial,
+ pipe,
+ abort() {
+ abortSignal = true
+ },
+ forceStop(last: string) {
+ abortSignal = true
+ initial = last
+ },
+ next(string: string) {
+ initial = string
+ continueSignal = true
+ },
+ restartBeforeTransform(string?: string) {
+ if (string != undefined) initial = string
+ pipeIndex = 0
+ },
+ moveToPipeIndex(index: number) {
+ pipeIndex = index
+ },
+ restartAfterTransform(string?: string) {
+ if (string != undefined) initial = string
+ restartAfterTransform = true
+ }
+ }
+ )
+
+
+ if (continueSignal && !restartAfterTransform) continue;
+
+ if (abortSignal) break;
+ initial = pipe[pipeIndex](initial)
+ history.push({
+ operation: pipe[pipeIndex].name,
+ value: initial
+ })
+
+ if (restartAfterTransform) { pipeIndex = -1 }
+
+ }
+
+ return {
+ history, output: initial
+ }
+
+}
+
+export interface PipeAPI {
+ effect(callback: EffectArgs): PipeAPI,
+ flow(...pipe: ((data: string) => string)[]): string,
+ raw(...pipe: ((data: string) => string)[]): ReturnType
+}
+//lighter pipe for more readable application
+export function pipe(initial: string): PipeAPI {
+
+ const state: {
+
+ called: {
+ pipe: boolean,
+ effect?: EffectArgs,
+ },
+ initial: string
+
+ } = {
+ called: {
+ pipe: false,
+ },
+ initial
+ };
+
+ const api = new Proxy({}, {
+ get(_, prop) {
+ if (prop === "flow") {
+ //pipe function
+ return (...pipe: ((data: string) => string)[]) => {
+
+ state.called.pipe = true;
+
+ const output = pipeLine({
+ initial: state.initial,
+ pipe,
+ effect: state.called.effect
+ })
+
+ return output.output
+
+
+ }
+ } else if (prop === "effect") {
+
+ return (effect?: EffectArgs) => {
+ state.called.effect = effect
+ return api
+ }
+
+ } else if (prop === "raw") {
+
+ return (...pipe: ((data: string) => string)[]) => {
+
+ state.called.pipe = true;
+
+ const output = pipeLine({
+ initial: state.initial,
+ pipe,
+ effect: state.called.effect
+ })
+
+ return output
+
+ }
+
+ }
+ }
+ })
+
+ //@ts-ignore
+ return api
+
+}
\ No newline at end of file