Skip to content

Commit

Permalink
Add index property to Transform (#619)
Browse files Browse the repository at this point in the history
  • Loading branch information
isaacs committed Sep 1, 2023
1 parent 968466f commit 0ee8e89
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 45 deletions.
34 changes: 33 additions & 1 deletion readme.md
Expand Up @@ -1373,7 +1373,33 @@ render(<Example />);

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}) => (
<Transform transform={(line, index) =>
index === 0 ? line : (' '.repeat(indent) + line)} {...props}>
{children}
</Transform>
);

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(<HangingIndent bold dimColor indent={4}>{text}</HangingIndent>);
```

#### transform(outputLine, index)

Type: `Function`

Expand All @@ -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?)
Expand Down
2 changes: 1 addition & 1 deletion src/components/Transform.tsx
Expand Up @@ -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;
};
Expand Down
2 changes: 1 addition & 1 deletion src/global.d.ts
Expand Up @@ -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;
};
}
4 changes: 2 additions & 2 deletions src/output.ts
Expand Up @@ -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`
Expand All @@ -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));
Expand Down
2 changes: 1 addition & 1 deletion src/render-node-to-output.ts
Expand Up @@ -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 = (
Expand Down
44 changes: 22 additions & 22 deletions src/squash-text-nodes.ts
Expand Up @@ -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;
Expand Down
59 changes: 42 additions & 17 deletions 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(<Text>Hello World</Text>);
Expand Down Expand Up @@ -253,46 +253,71 @@ test('fragment', t => {

test('transform children', t => {
const output = renderToString(
<Transform transform={(string: string) => `[${string}]`}>
<Transform
transform={(string: string, index: number) => `[${index}: ${string}]`}
>
<Text>
<Transform transform={(string: string) => `{${string}}`}>
<Transform
transform={(string: string, index: number) => `{${index}: ${string}}`}
>
<Text>test</Text>
</Transform>
</Text>
</Transform>
);

t.is(output, '[{test}]');
t.is(output, '[0: {0: test}]');
});

test('squash multiple text nodes', t => {
const output = renderToString(
<Transform transform={(string: string) => `[${string}]`}>
<Transform
transform={(string: string, index: number) => `[${index}: ${string}]`}
>
<Text>
<Transform transform={(string: string) => `{${string}}`}>
<Transform
transform={(string: string, index: number) => `{${index}: ${string}}`}
>
{/* prettier-ignore */}
<Text>hello{' '}world</Text>
</Transform>
</Text>
</Transform>
);

t.is(output, '[{hello world}]');
t.is(output, '[0: {0: hello world}]');
});

test('transform with multiple lines', t => {
const output = renderToString(
<Transform
transform={(string: string, index: number) => `[${index}: ${string}]`}
>
{/* prettier-ignore */}
<Text>hello{' '}world{'\n'}goodbye{' '}world</Text>
</Transform>
);

t.is(output, '[0: hello world]\n[1: goodbye world]');
});

test('squash multiple nested text nodes', t => {
const output = renderToString(
<Transform transform={(string: string) => `[${string}]`}>
<Transform
transform={(string: string, index: number) => `[${index}: ${string}]`}
>
<Text>
<Transform transform={(string: string) => `{${string}}`}>
<Transform
transform={(string: string, index: number) => `{${index}: ${string}}`}
>
hello
<Text> world</Text>
</Transform>
</Text>
</Transform>
);

t.is(output, '[{hello world}]');
t.is(output, '[0: {0: hello world}]');
});

test('squash empty `<Text>` nodes', t => {
Expand Down

0 comments on commit 0ee8e89

Please sign in to comment.