style: 完成所有文件的lint

This commit is contained in:
claude-code-best
2026-05-01 21:39:30 +08:00
parent d136872cc9
commit 6182015005
1333 changed files with 68255 additions and 77882 deletions

View File

@@ -1,37 +1,37 @@
import type { Token, Tokens } from 'marked'
import React from 'react'
import stripAnsi from 'strip-ansi'
import { useTerminalSize } from '../hooks/useTerminalSize.js'
import { Ansi, stringWidth, useTheme, wrapAnsi } from '@anthropic/ink'
import type { CliHighlight } from '../utils/cliHighlight.js'
import { formatToken, padAligned } from '../utils/markdown.js'
import type { Token, Tokens } from 'marked';
import React from 'react';
import stripAnsi from 'strip-ansi';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { Ansi, stringWidth, useTheme, wrapAnsi } from '@anthropic/ink';
import type { CliHighlight } from '../utils/cliHighlight.js';
import { formatToken, padAligned } from '../utils/markdown.js';
/** Accounts for parent indentation (e.g. message dot prefix) and terminal
* resize races. Without enough margin the table overflows its layout box
* and Ink's clip truncates differently on alternating frames, causing an
* infinite flicker loop in scrollback. */
const SAFETY_MARGIN = 4
const SAFETY_MARGIN = 4;
/** Minimum column width to prevent degenerate layouts */
const MIN_COLUMN_WIDTH = 3
const MIN_COLUMN_WIDTH = 3;
/**
* Maximum number of lines per row before switching to vertical format.
* When wrapping would make rows taller than this, vertical (key-value)
* format provides better readability.
*/
const MAX_ROW_LINES = 4
const MAX_ROW_LINES = 4;
/** ANSI escape codes for text formatting */
const ANSI_BOLD_START = '\x1b[1m'
const ANSI_BOLD_END = '\x1b[22m'
const ANSI_BOLD_START = '\x1b[1m';
const ANSI_BOLD_END = '\x1b[22m';
type Props = {
token: Tokens.Table
highlight: CliHighlight | null
token: Tokens.Table;
highlight: CliHighlight | null;
/** Override terminal width (useful for testing) */
forceWidth?: number
}
forceWidth?: number;
};
/**
* Wrap text to fit within a given width, returning array of lines.
@@ -40,26 +40,22 @@ type Props = {
* @param hard - If true, break words that exceed width (needed when columns
* are narrower than the longest word). Default false.
*/
function wrapText(
text: string,
width: number,
options?: { hard?: boolean },
): string[] {
if (width <= 0) return [text]
function wrapText(text: string, width: number, options?: { hard?: boolean }): string[] {
if (width <= 0) return [text];
// Strip trailing whitespace/newlines before wrapping.
// formatToken() adds EOL to paragraphs and other token types,
// which would otherwise create extra blank lines in table cells.
const trimmedText = text.trimEnd()
const trimmedText = text.trimEnd();
const wrapped = wrapAnsi(trimmedText, width, {
hard: options?.hard ?? false,
trim: false,
wordWrap: true,
})
});
// Filter out empty lines that result from trailing newlines or
// multiple consecutive newlines in the source content.
const lines = wrapped.split('\n').filter(line => line.length > 0)
const lines = wrapped.split('\n').filter(line => line.length > 0);
// Ensure we always return at least one line (empty string for empty cells)
return lines.length > 0 ? lines : ['']
return lines.length > 0 ? lines : [''];
}
/**
@@ -70,174 +66,152 @@ function wrapText(
* 3. Wrapping text within cells (no truncation)
* 4. Properly aligning multi-line rows with borders
*/
export function MarkdownTable({
token,
highlight,
forceWidth,
}: Props): React.ReactNode {
const [theme] = useTheme()
const { columns: actualTerminalWidth } = useTerminalSize()
const terminalWidth = forceWidth ?? actualTerminalWidth
export function MarkdownTable({ token, highlight, forceWidth }: Props): React.ReactNode {
const [theme] = useTheme();
const { columns: actualTerminalWidth } = useTerminalSize();
const terminalWidth = forceWidth ?? actualTerminalWidth;
// Format cell content to ANSI string
function formatCell(tokens: Token[] | undefined): string {
return (
tokens
?.map(_ => formatToken(_, theme, 0, null, null, highlight))
.join('') ?? ''
)
return tokens?.map(_ => formatToken(_, theme, 0, null, null, highlight)).join('') ?? '';
}
// Get plain text (stripped of ANSI codes)
function getPlainText(tokens: Token[] | undefined): string {
return stripAnsi(formatCell(tokens))
return stripAnsi(formatCell(tokens));
}
// Get the longest word width in a cell (minimum width to avoid breaking words)
function getMinWidth(tokens: Token[] | undefined): number {
const text = getPlainText(tokens)
const words = text.split(/\s+/).filter(w => w.length > 0)
if (words.length === 0) return MIN_COLUMN_WIDTH
return Math.max(...words.map(w => stringWidth(w)), MIN_COLUMN_WIDTH)
const text = getPlainText(tokens);
const words = text.split(/\s+/).filter(w => w.length > 0);
if (words.length === 0) return MIN_COLUMN_WIDTH;
return Math.max(...words.map(w => stringWidth(w)), MIN_COLUMN_WIDTH);
}
// Get ideal width (full content without wrapping)
function getIdealWidth(tokens: Token[] | undefined): number {
return Math.max(stringWidth(getPlainText(tokens)), MIN_COLUMN_WIDTH)
return Math.max(stringWidth(getPlainText(tokens)), MIN_COLUMN_WIDTH);
}
// Calculate column widths
// Step 1: Get minimum (longest word) and ideal (full content) widths
const minWidths = token.header.map((header, colIndex) => {
let maxMinWidth = getMinWidth(header.tokens)
let maxMinWidth = getMinWidth(header.tokens);
for (const row of token.rows) {
maxMinWidth = Math.max(maxMinWidth, getMinWidth(row[colIndex]?.tokens))
maxMinWidth = Math.max(maxMinWidth, getMinWidth(row[colIndex]?.tokens));
}
return maxMinWidth
})
return maxMinWidth;
});
const idealWidths = token.header.map((header, colIndex) => {
let maxIdeal = getIdealWidth(header.tokens)
let maxIdeal = getIdealWidth(header.tokens);
for (const row of token.rows) {
maxIdeal = Math.max(maxIdeal, getIdealWidth(row[colIndex]?.tokens))
maxIdeal = Math.max(maxIdeal, getIdealWidth(row[colIndex]?.tokens));
}
return maxIdeal
})
return maxIdeal;
});
// Step 2: Calculate available space
// Border overhead: │ content │ content │ = 1 + (width + 3) per column
const numCols = token.header.length
const borderOverhead = 1 + numCols * 3 // │ + (2 padding + 1 border) per col
const numCols = token.header.length;
const borderOverhead = 1 + numCols * 3; // │ + (2 padding + 1 border) per col
// Account for SAFETY_MARGIN to avoid triggering the fallback safety check
const availableWidth = Math.max(
terminalWidth - borderOverhead - SAFETY_MARGIN,
numCols * MIN_COLUMN_WIDTH,
)
const availableWidth = Math.max(terminalWidth - borderOverhead - SAFETY_MARGIN, numCols * MIN_COLUMN_WIDTH);
// Step 3: Calculate column widths that fit available space
const totalMin = minWidths.reduce((sum, w) => sum + w, 0)
const totalIdeal = idealWidths.reduce((sum, w) => sum + w, 0)
const totalMin = minWidths.reduce((sum, w) => sum + w, 0);
const totalIdeal = idealWidths.reduce((sum, w) => sum + w, 0);
// Track whether columns are narrower than longest words (needs hard wrap)
let needsHardWrap = false
let needsHardWrap = false;
let columnWidths: number[]
let columnWidths: number[];
if (totalIdeal <= availableWidth) {
// Everything fits - use ideal widths
columnWidths = idealWidths
columnWidths = idealWidths;
} else if (totalMin <= availableWidth) {
// Need to shrink - give each column its min, distribute remaining space
const extraSpace = availableWidth - totalMin
const overflows = idealWidths.map((ideal, i) => ideal - minWidths[i]!)
const totalOverflow = overflows.reduce((sum, o) => sum + o, 0)
const extraSpace = availableWidth - totalMin;
const overflows = idealWidths.map((ideal, i) => ideal - minWidths[i]!);
const totalOverflow = overflows.reduce((sum, o) => sum + o, 0);
columnWidths = minWidths.map((min, i) => {
if (totalOverflow === 0) return min
const extra = Math.floor((overflows[i]! / totalOverflow) * extraSpace)
return min + extra
})
if (totalOverflow === 0) return min;
const extra = Math.floor((overflows[i]! / totalOverflow) * extraSpace);
return min + extra;
});
} else {
// Table wider than terminal at minimum widths
// Shrink columns proportionally to fit, allowing word breaks
needsHardWrap = true
const scaleFactor = availableWidth / totalMin
columnWidths = minWidths.map(w =>
Math.max(Math.floor(w * scaleFactor), MIN_COLUMN_WIDTH),
)
needsHardWrap = true;
const scaleFactor = availableWidth / totalMin;
columnWidths = minWidths.map(w => Math.max(Math.floor(w * scaleFactor), MIN_COLUMN_WIDTH));
}
// Step 4: Calculate max row lines to determine if vertical format is needed
function calculateMaxRowLines(): number {
let maxLines = 1
let maxLines = 1;
// Check header
for (let i = 0; i < token.header.length; i++) {
const content = formatCell(token.header[i]!.tokens)
const content = formatCell(token.header[i]!.tokens);
const wrapped = wrapText(content, columnWidths[i]!, {
hard: needsHardWrap,
})
maxLines = Math.max(maxLines, wrapped.length)
});
maxLines = Math.max(maxLines, wrapped.length);
}
// Check rows
for (const row of token.rows) {
for (let i = 0; i < row.length; i++) {
const content = formatCell(row[i]?.tokens)
const content = formatCell(row[i]?.tokens);
const wrapped = wrapText(content, columnWidths[i]!, {
hard: needsHardWrap,
})
maxLines = Math.max(maxLines, wrapped.length)
});
maxLines = Math.max(maxLines, wrapped.length);
}
}
return maxLines
return maxLines;
}
// Use vertical format if wrapping would make rows too tall
const maxRowLines = calculateMaxRowLines()
const useVerticalFormat = maxRowLines > MAX_ROW_LINES
const maxRowLines = calculateMaxRowLines();
const useVerticalFormat = maxRowLines > MAX_ROW_LINES;
// Render a single row with potential multi-line cells
// Returns an array of strings, one per line of the row
function renderRowLines(
cells: Array<{ tokens?: Token[] }>,
isHeader: boolean,
): string[] {
function renderRowLines(cells: Array<{ tokens?: Token[] }>, isHeader: boolean): string[] {
// Get wrapped lines for each cell (preserving ANSI formatting)
const cellLines = cells.map((cell, colIndex) => {
const formattedText = formatCell(cell.tokens)
const width = columnWidths[colIndex]!
return wrapText(formattedText, width, { hard: needsHardWrap })
})
const formattedText = formatCell(cell.tokens);
const width = columnWidths[colIndex]!;
return wrapText(formattedText, width, { hard: needsHardWrap });
});
// Find max number of lines in this row
const maxLines = Math.max(...cellLines.map(lines => lines.length), 1)
const maxLines = Math.max(...cellLines.map(lines => lines.length), 1);
// Calculate vertical offset for each cell (to center vertically)
const verticalOffsets = cellLines.map(lines =>
Math.floor((maxLines - lines.length) / 2),
)
const verticalOffsets = cellLines.map(lines => Math.floor((maxLines - lines.length) / 2));
// Build each line of the row as a single string
const result: string[] = []
const result: string[] = [];
for (let lineIdx = 0; lineIdx < maxLines; lineIdx++) {
let line = '│'
let line = '│';
for (let colIndex = 0; colIndex < cells.length; colIndex++) {
const lines = cellLines[colIndex]!
const offset = verticalOffsets[colIndex]!
const contentLineIdx = lineIdx - offset
const lineText =
contentLineIdx >= 0 && contentLineIdx < lines.length
? lines[contentLineIdx]!
: ''
const width = columnWidths[colIndex]!
const lines = cellLines[colIndex]!;
const offset = verticalOffsets[colIndex]!;
const contentLineIdx = lineIdx - offset;
const lineText = contentLineIdx >= 0 && contentLineIdx < lines.length ? lines[contentLineIdx]! : '';
const width = columnWidths[colIndex]!;
// Headers always centered; data uses table alignment
const align = isHeader ? 'center' : (token.align?.[colIndex] ?? 'left')
const align = isHeader ? 'center' : (token.align?.[colIndex] ?? 'left');
line +=
' ' + padAligned(lineText, stringWidth(lineText), width, align) + ' │'
line += ' ' + padAligned(lineText, stringWidth(lineText), width, align) + ' │';
}
result.push(line)
result.push(line);
}
return result
return result;
}
// Render horizontal border as a single string
@@ -246,109 +220,102 @@ export function MarkdownTable({
top: ['┌', '─', '┬', '┐'],
middle: ['├', '─', '┼', '┤'],
bottom: ['└', '─', '┴', '┘'],
}[type] as [string, string, string, string]
}[type] as [string, string, string, string];
let line = left
let line = left;
columnWidths.forEach((width, colIndex) => {
line += mid.repeat(width + 2)
line += colIndex < columnWidths.length - 1 ? cross : right
})
return line
line += mid.repeat(width + 2);
line += colIndex < columnWidths.length - 1 ? cross : right;
});
return line;
}
// Render vertical format (key-value pairs) for extra-narrow terminals
function renderVerticalFormat(): string {
const lines: string[] = []
const headers = token.header.map(h => getPlainText(h.tokens))
const separatorWidth = Math.min(terminalWidth - 1, 40)
const separator = '─'.repeat(separatorWidth)
const lines: string[] = [];
const headers = token.header.map(h => getPlainText(h.tokens));
const separatorWidth = Math.min(terminalWidth - 1, 40);
const separator = '─'.repeat(separatorWidth);
// Small indent for wrapped lines (just 2 spaces)
const wrapIndent = ' '
const wrapIndent = ' ';
token.rows.forEach((row, rowIndex) => {
if (rowIndex > 0) {
lines.push(separator)
lines.push(separator);
}
row.forEach((cell, colIndex) => {
const label = headers[colIndex] || `Column ${colIndex + 1}`
const label = headers[colIndex] || `Column ${colIndex + 1}`;
// Clean value: trim, remove extra internal whitespace/newlines
const rawValue = formatCell(cell.tokens).trimEnd()
const value = rawValue.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim()
const rawValue = formatCell(cell.tokens).trimEnd();
const value = rawValue.replace(/\n+/g, ' ').replace(/\s+/g, ' ').trim();
// Wrap value to fit terminal, accounting for label on first line
const firstLineWidth = terminalWidth - stringWidth(label) - 3
const subsequentLineWidth = terminalWidth - wrapIndent.length - 1
const firstLineWidth = terminalWidth - stringWidth(label) - 3;
const subsequentLineWidth = terminalWidth - wrapIndent.length - 1;
// Two-pass wrap: first line is narrower (label takes space),
// continuation lines get the full width minus indent.
const firstPassLines = wrapText(value, Math.max(firstLineWidth, 10))
const firstLine = firstPassLines[0] || ''
const firstPassLines = wrapText(value, Math.max(firstLineWidth, 10));
const firstLine = firstPassLines[0] || '';
let wrappedValue: string[]
if (
firstPassLines.length <= 1 ||
subsequentLineWidth <= firstLineWidth
) {
wrappedValue = firstPassLines
let wrappedValue: string[];
if (firstPassLines.length <= 1 || subsequentLineWidth <= firstLineWidth) {
wrappedValue = firstPassLines;
} else {
// Re-join remaining text and re-wrap to the wider continuation width
const remainingText = firstPassLines
.slice(1)
.map(l => l.trim())
.join(' ')
const rewrapped = wrapText(remainingText, subsequentLineWidth)
wrappedValue = [firstLine, ...rewrapped]
.join(' ');
const rewrapped = wrapText(remainingText, subsequentLineWidth);
wrappedValue = [firstLine, ...rewrapped];
}
// First line: bold label + value
lines.push(
`${ANSI_BOLD_START}${label}:${ANSI_BOLD_END} ${wrappedValue[0] || ''}`,
)
lines.push(`${ANSI_BOLD_START}${label}:${ANSI_BOLD_END} ${wrappedValue[0] || ''}`);
// Subsequent lines with small indent (skip empty lines)
for (let i = 1; i < wrappedValue.length; i++) {
const line = wrappedValue[i]!
if (!line.trim()) continue
lines.push(`${wrapIndent}${line}`)
const line = wrappedValue[i]!;
if (!line.trim()) continue;
lines.push(`${wrapIndent}${line}`);
}
})
})
});
});
return lines.join('\n')
return lines.join('\n');
}
// Choose format based on available width
if (useVerticalFormat) {
return <Ansi>{renderVerticalFormat()}</Ansi>
return <Ansi>{renderVerticalFormat()}</Ansi>;
}
// Build the complete horizontal table as an array of strings
const tableLines: string[] = []
tableLines.push(renderBorderLine('top'))
tableLines.push(...renderRowLines(token.header, true))
tableLines.push(renderBorderLine('middle'))
const tableLines: string[] = [];
tableLines.push(renderBorderLine('top'));
tableLines.push(...renderRowLines(token.header, true));
tableLines.push(renderBorderLine('middle'));
token.rows.forEach((row, rowIndex) => {
tableLines.push(...renderRowLines(row, false))
tableLines.push(...renderRowLines(row, false));
if (rowIndex < token.rows.length - 1) {
tableLines.push(renderBorderLine('middle'))
tableLines.push(renderBorderLine('middle'));
}
})
tableLines.push(renderBorderLine('bottom'))
});
tableLines.push(renderBorderLine('bottom'));
// Safety check: verify no line exceeds terminal width.
// This catches edge cases during terminal resize where calculations
// were based on a different width than the current render target.
const maxLineWidth = Math.max(
...tableLines.map(line => stringWidth(stripAnsi(line))),
)
const maxLineWidth = Math.max(...tableLines.map(line => stringWidth(stripAnsi(line))));
// If we're within SAFETY_MARGIN characters of the edge, use vertical format
// to account for terminal resize race conditions.
if (maxLineWidth > terminalWidth - SAFETY_MARGIN) {
return <Ansi>{renderVerticalFormat()}</Ansi>
return <Ansi>{renderVerticalFormat()}</Ansi>;
}
// Render as a single Ansi block to prevent Ink from wrapping mid-row
return <Ansi>{tableLines.join('\n')}</Ansi>
return <Ansi>{tableLines.join('\n')}</Ansi>;
}