parseAnsi function

List<StyledSpan> parseAnsi(
  1. String input
)

Parse ANSI escape sequences in text.

Implementation

List<StyledSpan> parseAnsi(String input) {
  final spans = <StyledSpan>[];
  var style = const AnsiStyle();
  final buffer = StringBuffer();
  var i = 0;

  void flushBuffer() {
    if (buffer.isNotEmpty) {
      spans.add(StyledSpan(buffer.toString(), style));
      buffer.clear();
    }
  }

  while (i < input.length) {
    // Check for ESC[
    if (i + 1 < input.length &&
        input.codeUnitAt(i) == 0x1B &&
        input[i + 1] == '[') {
      flushBuffer();
      i += 2;

      // Parse CSI sequence
      final paramStart = i;
      while (i < input.length &&
          (input.codeUnitAt(i) >= 0x30 && input.codeUnitAt(i) <= 0x3F)) {
        i++;
      }
      // Skip intermediate bytes
      while (i < input.length &&
          (input.codeUnitAt(i) >= 0x20 && input.codeUnitAt(i) <= 0x2F)) {
        i++;
      }
      // Final byte
      if (i < input.length) {
        final finalByte = input[i];
        i++;

        if (finalByte == 'm') {
          // SGR sequence
          final params = input
              .substring(paramStart, i - 1)
              .split(';')
              .map((s) => int.tryParse(s) ?? 0)
              .toList();

          if (params.isEmpty || (params.length == 1 && params[0] == 0)) {
            style = const AnsiStyle();
          } else {
            var j = 0;
            while (j < params.length) {
              final p = params[j];
              switch (p) {
                case 0:
                  style = const AnsiStyle();
                  break;
                case 1:
                  style = style.copyWith(bold: true);
                  break;
                case 2:
                  style = style.copyWith(dim: true);
                  break;
                case 3:
                  style = style.copyWith(italic: true);
                  break;
                case 4:
                  style = style.copyWith(underline: true);
                  break;
                case 7:
                  style = style.copyWith(inverse: true);
                  break;
                case 9:
                  style = style.copyWith(strikethrough: true);
                  break;
                case 22:
                  style = style.copyWith(bold: false, dim: false);
                  break;
                case 23:
                  style = style.copyWith(italic: false);
                  break;
                case 24:
                  style = style.copyWith(underline: false);
                  break;
                case 27:
                  style = style.copyWith(inverse: false);
                  break;
                case 29:
                  style = style.copyWith(strikethrough: false);
                  break;
                case >= 30 && <= 37:
                  style = style.copyWith(
                    foreground: AnsiColors.standard[p - 30],
                  );
                  break;
                case 38:
                  // Extended foreground
                  if (j + 1 < params.length &&
                      params[j + 1] == 5 &&
                      j + 2 < params.length) {
                    style = style.copyWith(
                      foreground: AnsiColors.color256(params[j + 2]),
                    );
                    j += 2;
                  } else if (j + 1 < params.length &&
                      params[j + 1] == 2 &&
                      j + 4 < params.length) {
                    style = style.copyWith(
                      foreground: Color.fromARGB(
                        255,
                        params[j + 2],
                        params[j + 3],
                        params[j + 4],
                      ),
                    );
                    j += 4;
                  }
                  break;
                case 39:
                  style = style.copyWith(foreground: null);
                  break;
                case >= 40 && <= 47:
                  style = style.copyWith(
                    background: AnsiColors.standard[p - 40],
                  );
                  break;
                case 48:
                  // Extended background
                  if (j + 1 < params.length &&
                      params[j + 1] == 5 &&
                      j + 2 < params.length) {
                    style = style.copyWith(
                      background: AnsiColors.color256(params[j + 2]),
                    );
                    j += 2;
                  } else if (j + 1 < params.length &&
                      params[j + 1] == 2 &&
                      j + 4 < params.length) {
                    style = style.copyWith(
                      background: Color.fromARGB(
                        255,
                        params[j + 2],
                        params[j + 3],
                        params[j + 4],
                      ),
                    );
                    j += 4;
                  }
                  break;
                case 49:
                  style = style.copyWith(background: null);
                  break;
                case >= 90 && <= 97:
                  style = style.copyWith(
                    foreground: AnsiColors.standard[p - 90 + 8],
                  );
                  break;
                case >= 100 && <= 107:
                  style = style.copyWith(
                    background: AnsiColors.standard[p - 100 + 8],
                  );
                  break;
              }
              j++;
            }
          }
        }
        // Ignore other CSI sequences (cursor movement, etc.)
      }
    } else {
      buffer.writeCharCode(input.codeUnitAt(i));
      i++;
    }
  }

  flushBuffer();
  return spans;
}