diff --git a/readme.md b/readme.md
index 573c47279..7fe0236c5 100644
--- a/readme.md
+++ b/readme.md
@@ -1373,7 +1373,33 @@ render();
Since `transform` function converts all characters to upper case, final output that's rendered to the terminal will be "HELLO WORLD", not "Hello World".
-#### transform(children)
+When the output wraps to multiple lines, it can be helpful to know which line is being processed.
+
+For example, to implement a hanging indent component, you can indent all the lines except for the first.
+
+```jsx
+import {render, Transform} from 'ink';
+
+const HangingIndent = ({content, indent = 4, children, ...props}) => (
+
+ index === 0 ? line : (' '.repeat(indent) + line)} {...props}>
+ {children}
+
+);
+
+const text =
+ 'WHEN I WROTE the following pages, or rather the bulk of them, ' +
+ 'I lived alone, in the woods, a mile from any neighbor, in a ' +
+ 'house which I had built myself, on the shore of Walden Pond, ' +
+ 'in Concord, Massachusetts, and earned my living by the labor ' +
+ 'of my hands only. I lived there two years and two months. At ' +
+ 'present I am a sojourner in civilized life again.';
+
+// Other text properties are allowed as well
+render({text});
+```
+
+#### transform(outputLine, index)
Type: `Function`
@@ -1386,6 +1412,12 @@ Type: `string`
Output of child components.
+##### index
+
+Type: `number`
+
+The zero-indexed line number of the line currently being transformed.
+
## Hooks
### useInput(inputHandler, options?)
diff --git a/src/components/Transform.tsx b/src/components/Transform.tsx
index 9d59f7233..43dbe9685 100644
--- a/src/components/Transform.tsx
+++ b/src/components/Transform.tsx
@@ -4,7 +4,7 @@ export type Props = {
/**
* Function which transforms children output. It accepts children and must return transformed children too.
*/
- readonly transform: (children: string) => string;
+ readonly transform: (children: string, index: number) => string;
readonly children?: ReactNode;
};
diff --git a/src/global.d.ts b/src/global.d.ts
index 002e2c2c6..494946e86 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -28,6 +28,6 @@ declare namespace Ink {
style?: Styles;
// eslint-disable-next-line @typescript-eslint/naming-convention
- internal_transform?: (children: string) => string;
+ internal_transform?: (children: string, index: number) => string;
};
}
diff --git a/src/output.ts b/src/output.ts
index 12286460b..d2ad872d9 100644
--- a/src/output.ts
+++ b/src/output.ts
@@ -187,7 +187,7 @@ export default class Output {
let offsetY = 0;
- for (let line of lines) {
+ for (let [index, line] of lines.entries()) {
const currentLine = output[y + offsetY];
// Line can be missing if `text` is taller than height of pre-initialized `this.output`
@@ -196,7 +196,7 @@ export default class Output {
}
for (const transformer of transformers) {
- line = transformer(line);
+ line = transformer(line, index);
}
const characters = styledCharsFromTokens(tokenize(line));
diff --git a/src/render-node-to-output.ts b/src/render-node-to-output.ts
index 011a105fb..73a7de1bf 100644
--- a/src/render-node-to-output.ts
+++ b/src/render-node-to-output.ts
@@ -27,7 +27,7 @@ const applyPaddingToText = (node: DOMElement, text: string): string => {
return text;
};
-export type OutputTransformer = (s: string) => string;
+export type OutputTransformer = (s: string, index: number) => string;
// After nodes are laid out, render each to output object, which later gets rendered to terminal
const renderNodeToOutput = (
diff --git a/src/squash-text-nodes.ts b/src/squash-text-nodes.ts
index 2e61817a3..f3fd0da22 100644
--- a/src/squash-text-nodes.ts
+++ b/src/squash-text-nodes.ts
@@ -9,32 +9,32 @@ import {type DOMElement} from './dom.js';
const squashTextNodes = (node: DOMElement): string => {
let text = '';
- if (node.childNodes.length > 0) {
- for (const childNode of node.childNodes) {
- let nodeText = '';
+ for (let index = 0; index < node.childNodes.length; index++) {
+ const childNode = node.childNodes[index];
+ if (childNode === undefined) continue;
+ let nodeText = '';
- if (childNode.nodeName === '#text') {
- nodeText = childNode.nodeValue;
- } else {
- if (
- childNode.nodeName === 'ink-text' ||
- childNode.nodeName === 'ink-virtual-text'
- ) {
- nodeText = squashTextNodes(childNode);
- }
-
- // Since these text nodes are being concatenated, `Output` instance won't be able to
- // apply children transform, so we have to do it manually here for each text node
- if (
- nodeText.length > 0 &&
- typeof childNode.internal_transform === 'function'
- ) {
- nodeText = childNode.internal_transform(nodeText);
- }
+ if (childNode.nodeName === '#text') {
+ nodeText = childNode.nodeValue;
+ } else {
+ if (
+ childNode.nodeName === 'ink-text' ||
+ childNode.nodeName === 'ink-virtual-text'
+ ) {
+ nodeText = squashTextNodes(childNode);
}
- text += nodeText;
+ // Since these text nodes are being concatenated, `Output` instance won't be able to
+ // apply children transform, so we have to do it manually here for each text node
+ if (
+ nodeText.length > 0 &&
+ typeof childNode.internal_transform === 'function'
+ ) {
+ nodeText = childNode.internal_transform(nodeText, index);
+ }
}
+
+ text += nodeText;
}
return text;
diff --git a/test/components.tsx b/test/components.tsx
index 0d2ef0829..51b61c446 100644
--- a/test/components.tsx
+++ b/test/components.tsx
@@ -1,21 +1,21 @@
import EventEmitter from 'node:events';
-import React, {useState, Component} from 'react';
+import test from 'ava';
import chalk from 'chalk';
+import React, {Component, useState} from 'react';
import {spy} from 'sinon';
-import test from 'ava';
import {
Box,
- Text,
- Static,
- Transform,
Newline,
+ render,
Spacer,
- useStdin,
- render
+ Static,
+ Text,
+ Transform,
+ useStdin
} from '../src/index.js';
+import createStdout from './helpers/create-stdout.js';
import {renderToString} from './helpers/render-to-string.js';
import {run} from './helpers/run.js';
-import createStdout from './helpers/create-stdout.js';
test('text', t => {
const output = renderToString(Hello World);
@@ -253,23 +253,31 @@ test('fragment', t => {
test('transform children', t => {
const output = renderToString(
- `[${string}]`}>
+ `[${index}: ${string}]`}
+ >
- `{${string}}`}>
+ `{${index}: ${string}}`}
+ >
test
);
- t.is(output, '[{test}]');
+ t.is(output, '[0: {0: test}]');
});
test('squash multiple text nodes', t => {
const output = renderToString(
- `[${string}]`}>
+ `[${index}: ${string}]`}
+ >
- `{${string}}`}>
+ `{${index}: ${string}}`}
+ >
{/* prettier-ignore */}
hello{' '}world
@@ -277,14 +285,31 @@ test('squash multiple text nodes', t => {
);
- t.is(output, '[{hello world}]');
+ t.is(output, '[0: {0: hello world}]');
+});
+
+test('transform with multiple lines', t => {
+ const output = renderToString(
+ `[${index}: ${string}]`}
+ >
+ {/* prettier-ignore */}
+ hello{' '}world{'\n'}goodbye{' '}world
+
+ );
+
+ t.is(output, '[0: hello world]\n[1: goodbye world]');
});
test('squash multiple nested text nodes', t => {
const output = renderToString(
- `[${string}]`}>
+ `[${index}: ${string}]`}
+ >
- `{${string}}`}>
+ `{${index}: ${string}}`}
+ >
hello
world
@@ -292,7 +317,7 @@ test('squash multiple nested text nodes', t => {
);
- t.is(output, '[{hello world}]');
+ t.is(output, '[0: {0: hello world}]');
});
test('squash empty `` nodes', t => {