render method

  1. @override
String render()
override

Renders the table to a string.

Implementation

@override
String render() {
  if (_headers.isEmpty && _rows.isEmpty) return '';

  final columns = _headers.isNotEmpty
      ? _headers.length
      : (_rows.isNotEmpty ? _rows.first.length : 0);

  // Calculate column widths using rendered cell content so width/align styles are respected.
  final widths = List<int>.filled(columns, 0);
  if (_manualWidths != null) {
    for (var i = 0; i < columns && i < _manualWidths!.length; i++) {
      widths[i] = _manualWidths![i];
    }
  }

  if (_manualWidths == null) {
    void applyWidths(List<String> cells, int rowIndex) {
      for (var c = 0; c < columns; c++) {
        final raw = c < cells.length ? cells[c] : '';
        final rendered = _renderCellValue(rowIndex, c, raw);
        for (final line in rendered.split('\n')) {
          final len = Style.visibleLength(line);
          if (len > widths[c]) widths[c] = len;
        }
      }
    }

    if (_headers.isNotEmpty) {
      applyWidths(_headers, headerRow);
    }

    for (var i = 0; i < _rows.length; i++) {
      applyWidths(_rows[i], i);
    }
  }

  // Adjust for fixed width if specified
  if (_width != null && _manualWidths == null) {
    final totalPadding = _padding * 2 * columns;
    final borderCount = _borderColumn ? columns + 1 : 2;
    final available = _width! - totalPadding - borderCount;
    if (available > 0) {
      final perColumn = available ~/ columns;
      for (var i = 0; i < widths.length; i++) {
        if (widths[i] < perColumn) widths[i] = perColumn;
      }

      // Distribute any remaining width across columns (left-to-right) so the
      // rendered table matches the requested total width.
      final used = widths.fold<int>(0, (sum, w) => sum + w);
      var remaining = available - used;
      var j = 0;
      while (remaining > 0 && widths.isNotEmpty) {
        widths[j % widths.length]++;
        remaining--;
        j++;
      }
    }
  }

  final pad = ' ' * _padding;
  final b = _border;

  // Helper to style border characters
  String styleBorderText(String text) {
    if (_borderStyle == null) return text;
    return _renderConfig.configureStyle(_borderStyle!).render(text);
  }

  // Build horizontal border line
  String buildBorder(String left, String mid, String right, String fill) {
    final parts = widths.map((w) => fill * (w + _padding * 2));
    final leftChar = _borderLeft ? left : '';
    final rightChar = _borderRight ? right : '';
    final midChar = _borderColumn ? mid : '';
    return styleBorderText('$leftChar${parts.join(midChar)}$rightChar');
  }

  // Build a row with optional styling - handles multi-line cells
  List<String> buildRow(List<String> cells, int rowIndex) {
    // First, process each cell and split into lines
    final cellLines = <List<String>>[];
    var maxLines = 1;

    for (var c = 0; c < columns; c++) {
      final raw = c < cells.length ? cells[c] : '';
      final styledContent = _renderCellValue(rowIndex, c, raw, widths[c]);

      // Split by newlines to handle multi-line content
      final lines = styledContent.split('\n');
      cellLines.add(lines);
      if (lines.length > maxLines) maxLines = lines.length;
    }

    // Build output rows
    final outputRows = <String>[];
    for (var lineIdx = 0; lineIdx < maxLines; lineIdx++) {
      final parts = <String>[];
      for (var c = 0; c < columns; c++) {
        final lines = cellLines[c];
        final line = lineIdx < lines.length ? lines[lineIdx] : '';
        final visible = Style.visibleLength(line);
        final fill = widths[c] - visible;
        final fillCount = fill > 0 ? fill : 0;
        final cellContent = '$pad$line${' ' * fillCount}$pad';
        parts.add(cellContent);
      }
      final leftBorder = _borderLeft ? styleBorderText(b.left) : '';
      final rightBorder = _borderRight ? styleBorderText(b.right) : '';
      final colSep = _borderColumn ? styleBorderText(b.left) : '';
      outputRows.add('$leftBorder${parts.join(colSep)}$rightBorder');
    }

    return outputRows;
  }

  final buffer = StringBuffer();

  // Top border
  if (_borderTop) {
    buffer.writeln(
      buildBorder(b.topLeft, b.middleTop ?? b.top, b.topRight, b.top),
    );
  }

  // Header row
  if (_headers.isNotEmpty) {
    for (final line in buildRow(_headers, headerRow)) {
      buffer.writeln(line);
    }
    // Header separator
    if (_borderHeader) {
      buffer.writeln(
        buildBorder(
          b.middleLeft ?? b.left,
          b.middle ?? b.top,
          b.middleRight ?? b.right,
          b.top,
        ),
      );
    }
  }

  // Data rows with offset and height
  final startRow = _offset.clamp(0, _rows.length);
  var endRow = _rows.length;
  if (_height != null) {
    // Calculate available rows based on height
    var usedLines = 0;
    if (_borderTop) usedLines++;
    if (_headers.isNotEmpty) usedLines++;
    if (_borderHeader && _headers.isNotEmpty) usedLines++;
    if (_borderBottom) usedLines++;

    final availableRows = _height! - usedLines;
    if (availableRows > 0 && startRow + availableRows < endRow) {
      endRow = startRow + availableRows;
    }
  }

  for (var i = startRow; i < endRow; i++) {
    for (final line in buildRow(_rows[i], i)) {
      buffer.writeln(line);
    }
    // Row separator (between rows, not after last)
    if (_borderRow && i < endRow - 1) {
      buffer.writeln(
        buildBorder(
          b.middleLeft ?? b.left,
          b.middle ?? b.top,
          b.middleRight ?? b.right,
          b.top,
        ),
      );
    }
  }

  // Bottom border
  if (_borderBottom) {
    buffer.write(
      buildBorder(
        b.bottomLeft,
        b.middleBottom ?? b.bottom,
        b.bottomRight,
        b.bottom,
      ),
    );
  }

  return buffer.toString().trimRight();
}