@ai-sdk/tui 0.0.0 → 1.0.0-canary.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,240 @@
1
+ import { visibleLength } from './terminal-text';
2
+
3
+ export type MarkdownToken =
4
+ | { type: 'text'; text: string }
5
+ | { type: 'bold'; text: string }
6
+ | { type: 'italic'; text: string }
7
+ | { type: 'code'; text: string };
8
+
9
+ type TableAlignment = 'left' | 'center' | 'right';
10
+
11
+ const ansi = {
12
+ bold: '\x1b[1m',
13
+ boldOff: '\x1b[22m',
14
+ italic: '\x1b[3m',
15
+ italicOff: '\x1b[23m',
16
+ };
17
+
18
+ const tableSeparator = '─';
19
+
20
+ export function renderMarkdown(input: string): string {
21
+ const lines = input.split('\n');
22
+ const output: string[] = [];
23
+
24
+ for (let index = 0; index < lines.length; index += 1) {
25
+ const table = parseTable(lines, index);
26
+
27
+ if (table != null) {
28
+ output.push(...renderTable(table));
29
+ index = table.endIndex - 1;
30
+ continue;
31
+ }
32
+
33
+ output.push(renderMarkdownLine(lines[index] ?? ''));
34
+ }
35
+
36
+ return output.join('\n');
37
+ }
38
+
39
+ function renderMarkdownLine(line: string): string {
40
+ if (line.startsWith('### ')) {
41
+ return renderInlineMarkdown(`▶ ${line.slice(4)}`);
42
+ }
43
+
44
+ if (line.startsWith('## ')) {
45
+ return renderInlineMarkdown(`■ ${line.slice(3)}`);
46
+ }
47
+
48
+ if (line.startsWith('# ')) {
49
+ return renderInlineMarkdown(`█ ${line.slice(2)}`);
50
+ }
51
+
52
+ const unorderedListItem = line.match(/^(\s*)[-+*]\s+(.*)$/);
53
+ if (unorderedListItem) {
54
+ const [, indentation, text = ''] = unorderedListItem;
55
+ return renderInlineMarkdown(
56
+ `${indentation}•${text.length > 0 ? ` ${text}` : ''}`,
57
+ );
58
+ }
59
+
60
+ if (/^\d+\. /.test(line)) {
61
+ return renderInlineMarkdown(line.replace(/^(\d+)\. /, '$1. '));
62
+ }
63
+
64
+ if (line.startsWith('> ')) {
65
+ return renderInlineMarkdown(`│ ${line.slice(2)}`);
66
+ }
67
+
68
+ return renderInlineMarkdown(line);
69
+ }
70
+
71
+ function renderInlineMarkdown(input: string): string {
72
+ return input
73
+ .replace(/`([^`]+)`/g, '$1')
74
+ .replace(/\*\*([^*\n]+)\*\*/g, `${ansi.bold}$1${ansi.boldOff}`)
75
+ .replace(/__([^_\n]+)__/g, `${ansi.bold}$1${ansi.boldOff}`)
76
+ .replace(/\*([^*\n]+)\*/g, `${ansi.italic}$1${ansi.italicOff}`)
77
+ .replace(/_([^_\n]+)_/g, `${ansi.italic}$1${ansi.italicOff}`);
78
+ }
79
+
80
+ type ParsedTable = {
81
+ alignments: TableAlignment[];
82
+ endIndex: number;
83
+ header: string[];
84
+ rows: string[][];
85
+ };
86
+
87
+ function parseTable(
88
+ lines: string[],
89
+ startIndex: number,
90
+ ): ParsedTable | undefined {
91
+ const header = parseTableCells(lines[startIndex] ?? '');
92
+ if (header == null || header.length < 2) {
93
+ return undefined;
94
+ }
95
+
96
+ const separatorCells = parseTableCells(lines[startIndex + 1] ?? '');
97
+ if (separatorCells == null || separatorCells.length !== header.length) {
98
+ return undefined;
99
+ }
100
+
101
+ const alignments = parseTableAlignments(separatorCells);
102
+ if (alignments == null) {
103
+ return undefined;
104
+ }
105
+
106
+ const rows: string[][] = [];
107
+ let endIndex = startIndex + 2;
108
+
109
+ while (endIndex < lines.length) {
110
+ const row = parseTableCells(lines[endIndex] ?? '');
111
+ if (row == null) {
112
+ break;
113
+ }
114
+
115
+ rows.push(normalizeTableRow(row, header.length));
116
+ endIndex += 1;
117
+ }
118
+
119
+ return {
120
+ alignments,
121
+ endIndex,
122
+ header,
123
+ rows,
124
+ };
125
+ }
126
+
127
+ function parseTableCells(line: string): string[] | undefined {
128
+ if (!line.includes('|')) {
129
+ return undefined;
130
+ }
131
+
132
+ let tableLine = line.trim();
133
+ if (tableLine.startsWith('|')) {
134
+ tableLine = tableLine.slice(1);
135
+ }
136
+ if (tableLine.endsWith('|') && !tableLine.endsWith('\\|')) {
137
+ tableLine = tableLine.slice(0, -1);
138
+ }
139
+
140
+ const cells: string[] = [];
141
+ let cell = '';
142
+
143
+ for (let index = 0; index < tableLine.length; index += 1) {
144
+ const character = tableLine[index];
145
+ const nextCharacter = tableLine[index + 1];
146
+
147
+ if (character === '\\' && nextCharacter === '|') {
148
+ cell += '|';
149
+ index += 1;
150
+ continue;
151
+ }
152
+
153
+ if (character === '|') {
154
+ cells.push(cell.trim());
155
+ cell = '';
156
+ continue;
157
+ }
158
+
159
+ cell += character;
160
+ }
161
+
162
+ cells.push(cell.trim());
163
+
164
+ return cells;
165
+ }
166
+
167
+ function parseTableAlignments(cells: string[]): TableAlignment[] | undefined {
168
+ const alignments: TableAlignment[] = [];
169
+
170
+ for (const cell of cells) {
171
+ const match = cell.match(/^(:)?-{3,}(:)?$/);
172
+ if (match == null) {
173
+ return undefined;
174
+ }
175
+
176
+ const [, left, right] = match;
177
+ alignments.push(
178
+ left != null && right != null
179
+ ? 'center'
180
+ : right != null
181
+ ? 'right'
182
+ : 'left',
183
+ );
184
+ }
185
+
186
+ return alignments;
187
+ }
188
+
189
+ function normalizeTableRow(row: string[], length: number): string[] {
190
+ return Array.from({ length }, (_, index) => row[index] ?? '');
191
+ }
192
+
193
+ function renderTable(table: ParsedTable): string[] {
194
+ const header = table.header.map(
195
+ cell => `${ansi.bold}${renderInlineMarkdown(cell)}${ansi.boldOff}`,
196
+ );
197
+ const rows = table.rows.map(row => row.map(renderInlineMarkdown));
198
+ const tableRows = [header, ...rows];
199
+ const widths = table.alignments.map((_, columnIndex) =>
200
+ Math.max(3, ...tableRows.map(row => visibleLength(row[columnIndex] ?? ''))),
201
+ );
202
+
203
+ return [
204
+ formatTableRow(header, widths, table.alignments),
205
+ widths.map(width => tableSeparator.repeat(width)).join(' '),
206
+ ...rows.map(row => formatTableRow(row, widths, table.alignments)),
207
+ ];
208
+ }
209
+
210
+ function formatTableRow(
211
+ row: string[],
212
+ widths: number[],
213
+ alignments: TableAlignment[],
214
+ ): string {
215
+ return row
216
+ .map((cell, index) =>
217
+ alignTableCell(cell, widths[index] ?? 0, alignments[index] ?? 'left'),
218
+ )
219
+ .join(' ');
220
+ }
221
+
222
+ function alignTableCell(
223
+ cell: string,
224
+ width: number,
225
+ alignment: TableAlignment,
226
+ ): string {
227
+ const paddingWidth = Math.max(0, width - visibleLength(cell));
228
+
229
+ if (alignment === 'right') {
230
+ return `${' '.repeat(paddingWidth)}${cell}`;
231
+ }
232
+
233
+ if (alignment === 'center') {
234
+ const leftPadding = Math.floor(paddingWidth / 2);
235
+ const rightPadding = paddingWidth - leftPadding;
236
+ return `${' '.repeat(leftPadding)}${cell}${' '.repeat(rightPadding)}`;
237
+ }
238
+
239
+ return `${cell}${' '.repeat(paddingWidth)}`;
240
+ }
@@ -0,0 +1,111 @@
1
+ export type TerminalFrameOutput = {
2
+ write(
3
+ chunk: string | Uint8Array,
4
+ encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
5
+ callback?: (error?: Error | null) => void,
6
+ ): boolean;
7
+ };
8
+
9
+ export type TerminalFrameBufferOptions = {
10
+ useSynchronizedUpdates?: boolean;
11
+ };
12
+
13
+ const escape = '\x1b';
14
+ const cursorHome = `${escape}[H`;
15
+ const clearScreen = `${escape}[2J`;
16
+ const clearLine = `${escape}[2K`;
17
+ const synchronizeStart = `${escape}[?2026h`;
18
+ const synchronizeEnd = `${escape}[?2026l`;
19
+
20
+ type FrameSnapshot = {
21
+ lines: string[];
22
+ };
23
+
24
+ export class TerminalFrameBuffer {
25
+ readonly #useSynchronizedUpdates: boolean;
26
+ readonly #originalWrite: TerminalFrameOutput['write'];
27
+
28
+ #previousFrame?: FrameSnapshot;
29
+ #isWritingFrame = false;
30
+ #externalWriteSinceLastFrame = false;
31
+
32
+ constructor(
33
+ output: TerminalFrameOutput,
34
+ options?: TerminalFrameBufferOptions,
35
+ ) {
36
+ this.#originalWrite = output.write.bind(output);
37
+ this.#useSynchronizedUpdates = options?.useSynchronizedUpdates ?? true;
38
+
39
+ output.write = ((
40
+ chunk: string | Uint8Array,
41
+ encodingOrCallback?: BufferEncoding | ((error?: Error | null) => void),
42
+ callback?: (error?: Error | null) => void,
43
+ ) => {
44
+ if (!this.#isWritingFrame) {
45
+ this.#externalWriteSinceLastFrame = true;
46
+ }
47
+
48
+ return this.#originalWrite(chunk, encodingOrCallback, callback);
49
+ }) as TerminalFrameOutput['write'];
50
+ }
51
+
52
+ present(frame: string) {
53
+ const nextFrame = snapshotFrame(frame);
54
+ const update =
55
+ this.#previousFrame && !this.#externalWriteSinceLastFrame
56
+ ? diffFrame(this.#previousFrame, nextFrame)
57
+ : `${cursorHome}${clearScreen}${frame}`;
58
+
59
+ this.#previousFrame = nextFrame;
60
+ this.#externalWriteSinceLastFrame = false;
61
+
62
+ if (update.length === 0) {
63
+ return;
64
+ }
65
+
66
+ this.#writeUpdate(update);
67
+ }
68
+
69
+ reset() {
70
+ this.#previousFrame = undefined;
71
+ }
72
+
73
+ #writeUpdate(update: string) {
74
+ this.#isWritingFrame = true;
75
+
76
+ try {
77
+ if (!this.#useSynchronizedUpdates) {
78
+ this.#originalWrite(update);
79
+ return;
80
+ }
81
+
82
+ this.#originalWrite(`${synchronizeStart}${update}${synchronizeEnd}`);
83
+ } finally {
84
+ this.#isWritingFrame = false;
85
+ }
86
+ }
87
+ }
88
+
89
+ function snapshotFrame(frame: string): FrameSnapshot {
90
+ return { lines: frame.split('\n') };
91
+ }
92
+
93
+ function diffFrame(previousFrame: FrameSnapshot, nextFrame: FrameSnapshot) {
94
+ let output = '';
95
+ const lineCount = Math.max(
96
+ previousFrame.lines.length,
97
+ nextFrame.lines.length,
98
+ );
99
+
100
+ for (let index = 0; index < lineCount; index++) {
101
+ const line = nextFrame.lines[index];
102
+
103
+ if (previousFrame.lines[index] === line) {
104
+ continue;
105
+ }
106
+
107
+ output += `${escape}[${index + 1};1H${clearLine}${line ?? ''}`;
108
+ }
109
+
110
+ return output;
111
+ }