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 => {