diff --git a/phpseclib/File/ANSI.php b/phpseclib/File/ANSI.php index ee33404a..2d03a199 100644 --- a/phpseclib/File/ANSI.php +++ b/phpseclib/File/ANSI.php @@ -101,6 +101,22 @@ class ANSI */ var $old_y; + /** + * An empty attribute cell + * + * @var Object + * @access private + */ + var $base_attr_cell; + + /** + * The current attribute cell + * + * @var Object + * @access private + */ + var $attr_cell; + /** * An empty attribute row * @@ -125,62 +141,6 @@ class ANSI */ var $attrs; - /** - * The current foreground color - * - * @var String - * @access private - */ - var $foreground; - - /** - * The current background color - * - * @var String - * @access private - */ - var $background; - - /** - * Bold flag - * - * @var Boolean - * @access private - */ - var $bold; - - /** - * Underline flag - * - * @var Boolean - * @access private - */ - var $underline; - - /** - * Blink flag - * - * @var Boolean - * @access private - */ - var $blink; - - /** - * Reverse flag - * - * @var Boolean - * @access private - */ - var $reverse; - - /** - * Color flag - * - * @var Boolean - * @access private - */ - var $color; - /** * Current ANSI code * @@ -189,6 +149,14 @@ class ANSI */ var $ansi; + /** + * Tokenization + * + * @var Array + * @access private + */ + var $tokenization; + /** * Default Constructor. * @@ -197,6 +165,16 @@ class ANSI */ function __construct() { + $attr_cell = new \stdClass(); + $attr_cell->bold = false; + $attr_cell->underline = false; + $attr_cell->blink = false; + $attr_cell->background = 'black'; + $attr_cell->foreground = 'white'; + $attr_cell->reverse = false; + $this->base_attr_cell = clone $attr_cell; + $this->attr_cell = clone $attr_cell; + $this->setHistory(200); $this->setDimensions(80, 24); } @@ -216,17 +194,9 @@ class ANSI $this->max_y = $y - 1; $this->x = $this->y = 0; $this->history = $this->history_attrs = array(); - $this->attr_row = array_fill(0, $this->max_x + 1, ''); + $this->attr_row = array_fill(0, $this->max_x + 2, $this->base_attr_cell); $this->screen = array_fill(0, $this->max_y + 1, ''); $this->attrs = array_fill(0, $this->max_y + 1, $this->attr_row); - $this->foreground = 'white'; - $this->background = 'black'; - $this->bold = false; - $this->underline = false; - $this->blink = false; - $this->reverse = false; - $this->color = false; - $this->ansi = ''; } @@ -262,6 +232,7 @@ class ANSI */ function appendString($source) { + $this->tokenization = array(''); for ($i = 0; $i < strlen($source); $i++) { if (strlen($this->ansi)) { $this->ansi.= $source[$i]; @@ -278,6 +249,8 @@ class ANSI default: continue 2; } + $this->tokenization[] = $this->ansi; + $this->tokenization[] = ''; // http://ascii-table.com/ansi-escape-sequences-vt-100.php switch ($this->ansi) { case "\x1B[H": // Move cursor to upper left corner @@ -299,7 +272,7 @@ class ANSI case "\x1B[K": // Clear screen from cursor right $this->screen[$this->y] = substr($this->screen[$this->y], 0, $this->x); - array_splice($this->attrs[$this->y], $this->x + 1); + array_splice($this->attrs[$this->y], $this->x + 1, $this->max_x - $this->x, array_fill($this->x, $this->max_x - $this->x - 1, $this->base_attr_cell)); break; case "\x1B[2K": // Clear entire line $this->screen[$this->y] = str_repeat(' ', $this->x); @@ -307,6 +280,7 @@ class ANSI break; case "\x1B[?1h": // set cursor key to application case "\x1B[?25h": // show the cursor + case "\x1B(B": // set united states g0 character set break; case "\x1BE": // Move to next line $this->_newLine(); @@ -314,6 +288,10 @@ class ANSI break; default: switch (true) { + case preg_match('#\x1B\[(\d+)B#', $this->ansi, $match): // Move cursor down n lines + $this->old_y = $this->y; + $this->y+= $match[1]; + break; case preg_match('#\x1B\[(\d+);(\d+)H#', $this->ansi, $match): // Move cursor to screen location v,h $this->old_x = $this->x; $this->old_y = $this->y; @@ -322,64 +300,42 @@ class ANSI break; case preg_match('#\x1B\[(\d+)C#', $this->ansi, $match): // Move cursor right n lines $this->old_x = $this->x; - $x = $match[1] - 1; + $this->x+= $match[1]; + break; + case preg_match('#\x1B\[(\d+)D#', $this->ansi, $match): // Move cursor left n lines + $this->old_x = $this->x; + $this->x-= $match[1]; break; case preg_match('#\x1B\[(\d+);(\d+)r#', $this->ansi, $match): // Set top and bottom lines of a window break; case preg_match('#\x1B\[(\d*(?:;\d*)*)m#', $this->ansi, $match): // character attributes + $attr_cell = &$this->attr_cell; $mods = explode(';', $match[1]); foreach ($mods as $mod) { switch ($mod) { case 0: // Turn off character attributes - $this->attrs[$this->y][$this->x] = ''; - - if ($this->bold) $this->attrs[$this->y][$this->x].= ''; - if ($this->underline) $this->attrs[$this->y][$this->x].= ''; - if ($this->blink) $this->attrs[$this->y][$this->x].= ''; - if ($this->color) $this->attrs[$this->y][$this->x].= ''; - - if ($this->reverse) { - $temp = $this->background; - $this->background = $this->foreground; - $this->foreground = $temp; - } - - $this->bold = $this->underline = $this->blink = $this->color = $this->reverse = false; + $attr_cell = clone $this->base_attr_cell; break; case 1: // Turn bold mode on - if (!$this->bold) { - $this->attrs[$this->y][$this->x] = ''; - $this->bold = true; - } + $attr_cell->bold = true; break; case 4: // Turn underline mode on - if (!$this->underline) { - $this->attrs[$this->y][$this->x] = ''; - $this->underline = true; - } + $attr_cell->underline = true; break; case 5: // Turn blinking mode on - if (!$this->blink) { - $this->attrs[$this->y][$this->x] = ''; - $this->blink = true; - } + $attr_cell->blink = true; break; case 7: // Turn reverse video on - $this->reverse = !$this->reverse; - $temp = $this->background; - $this->background = $this->foreground; - $this->foreground = $temp; - $this->attrs[$this->y][$this->x] = ''; - if ($this->color) { - $this->attrs[$this->y][$this->x] = '' . $this->attrs[$this->y][$this->x]; - } - $this->color = true; + $attr_cell->reverse = !$attr_cell->reverse; + $temp = $attr_cell->background; + $attr_cell->background = $attr_cell->foreground; + $attr_cell->foreground = $temp; break; default: // set colors - //$front = $this->reverse ? &$this->background : &$this->foreground; - $front = &$this->{ $this->reverse ? 'background' : 'foreground' }; - //$back = $this->reverse ? &$this->foreground : &$this->background; - $back = &$this->{ $this->reverse ? 'foreground' : 'background' }; + //$front = $attr_cell->reverse ? &$attr_cell->background : &$attr_cell->foreground; + $front = &$attr_cell->{ $attr_cell->reverse ? 'background' : 'foreground' }; + //$back = $attr_cell->reverse ? &$attr_cell->foreground : &$attr_cell->background; + $back = &$attr_cell->{ $attr_cell->reverse ? 'foreground' : 'background' }; switch ($mod) { case 30: $front = 'black'; break; case 31: $front = 'red'; break; @@ -400,28 +356,22 @@ class ANSI case 47: $back = 'white'; break; default: - user_error('Unsupported attribute: ' . $mod); + //user_error('Unsupported attribute: ' . $mod); $this->ansi = ''; break 2; } - - unset($temp); - $this->attrs[$this->y][$this->x] = ''; - if ($this->color) { - $this->attrs[$this->y][$this->x] = '' . $this->attrs[$this->y][$this->x]; - } - $this->color = true; } } break; default: - user_error("{$this->ansi} unsupported\r\n"); + //user_error("{$this->ansi} is unsupported\r\n"); } } $this->ansi = ''; continue; } + $this->tokenization[count($this->tokenization) - 1].= $source[$i]; switch ($source[$i]) { case "\r": $this->x = 0; @@ -429,12 +379,32 @@ class ANSI case "\n": $this->_newLine(); break; + case "\x08": // backspace + if ($this->x) { + $this->x--; + $this->attrs[$this->y][$this->x] = clone $this->base_attr_cell; + $this->screen[$this->y] = substr_replace( + $this->screen[$this->y], + $source[$i], + $this->x, + 1 + ); + } + break; case "\x0F": // shift break; case "\x1B": // start ANSI escape code + $this->tokenization[count($this->tokenization) - 1] = substr($this->tokenization[count($this->tokenization) - 1], 0, -1); + //if (!strlen($this->tokenization[count($this->tokenization) - 1])) { + // array_pop($this->tokenization); + //} $this->ansi.= "\x1B"; break; default: + $this->attrs[$this->y][$this->x] = clone $this->attr_cell; + if ($this->x > strlen($this->screen[$this->y])) { + $this->screen[$this->y] = str_repeat(' ', $this->x); + } $this->screen[$this->y] = substr_replace( $this->screen[$this->y], $source[$i], @@ -482,6 +452,63 @@ class ANSI $this->y++; } + /** + * Returns the current coordinate without preformating + * + * @access private + * @return String + */ + function _processCoordinate($last_attr, $cur_attr, $char) + { + $output = ''; + + if ($last_attr != $cur_attr) { + $close = $open = ''; + if ($last_attr->foreground != $cur_attr->foreground) { + if ($cur_attr->foreground != 'white') { + $open.= ''; + } + if ($last_attr->foreground != 'white') { + $close = '' . $close; + } + } + if ($last_attr->background != $cur_attr->background) { + if ($cur_attr->background != 'black') { + $open.= ''; + } + if ($last_attr->background != 'black') { + $close = '' . $close; + } + } + if ($last_attr->bold != $cur_attr->bold) { + if ($cur_attr->bold) { + $open.= ''; + } else { + $close = '' . $close; + } + } + if ($last_attr->underline != $cur_attr->underline) { + if ($cur_attr->underline) { + $open.= ''; + } else { + $close = '' . $close; + } + } + if ($last_attr->blink != $cur_attr->blink) { + if ($cur_attr->blink) { + $open.= ''; + } else { + $close = '' . $close; + } + } + $output.= $close . $open; + } + + $output.= htmlspecialchars($char); + + return $output; + } + /** * Returns the current screen without preformating * @@ -491,17 +518,18 @@ class ANSI function _getScreen() { $output = ''; + $last_attr = $this->base_attr_cell; for ($i = 0; $i <= $this->max_y; $i++) { - for ($j = 0; $j <= $this->max_x + 1; $j++) { - if (isset($this->attrs[$i][$j])) { - $output.= $this->attrs[$i][$j]; - } - if (isset($this->screen[$i][$j])) { - $output.= htmlspecialchars($this->screen[$i][$j]); - } + for ($j = 0; $j <= $this->max_x; $j++) { + $cur_attr = $this->attrs[$i][$j]; + $output.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->screen[$i][$j]) ? $this->screen[$i][$j] : ''); + $last_attr = $this->attrs[$i][$j]; } $output.= "\r\n"; } + $output = substr($output, 0, -2); + // close any remaining open tags + $output.= $this->_processCoordinate($last_attr, $this->base_attr_cell, ''); return rtrim($output); } @@ -513,7 +541,7 @@ class ANSI */ function getScreen() { - return '
' . $this->_getScreen() . '
'; + return '
' . $this->_getScreen() . '
'; } /** @@ -525,19 +553,20 @@ class ANSI function getHistory() { $scrollback = ''; + $last_attr = $this->base_attr_cell; for ($i = 0; $i < count($this->history); $i++) { for ($j = 0; $j <= $this->max_x + 1; $j++) { - if (isset($this->history_attrs[$i][$j])) { - $scrollback.= $this->history_attrs[$i][$j]; - } - if (isset($this->history[$i][$j])) { - $scrollback.= htmlspecialchars($this->history[$i][$j]); - } + $cur_attr = $this->history_attrs[$i][$j]; + $scrollback.= $this->_processCoordinate($last_attr, $cur_attr, isset($this->history[$i][$j]) ? $this->history[$i][$j] : ''); + $last_attr = $this->history_attrs[$i][$j]; } $scrollback.= "\r\n"; } + $base_attr_cell = $this->base_attr_cell; + $this->base_attr_cell = $last_attr; $scrollback.= $this->_getScreen(); + $this->base_attr_cell = $base_attr_cell; - return '
' . $scrollback . '
'; + return '
' . $scrollback . '
'; } } diff --git a/tests/Unit/File/ANSITest.php b/tests/Unit/File/ANSITest.php new file mode 100644 index 00000000..373504d1 --- /dev/null +++ b/tests/Unit/File/ANSITest.php @@ -0,0 +1,31 @@ + + * @copyright 2014 Jim Wigginton + * @license http://www.opensource.org/licenses/mit-license.html MIT License + */ + +use phpseclib\File\ANSI; + +class Unit_File_ANSITest extends PhpseclibTestCase +{ + public function testCase1() + { + $str = "\x1B[07m"; // turn reverse video on + $str.= "aaaaaaaaaaaaaaaaaa"; + $str.= "\x1B[10D"; // move cursor left 10 lines + $str.= "\x1B[m"; // reset everything + $str.= "bbb"; + + $ansi = new ANSI(); + $ansi->appendString($str); + + $expected = '
';
+        $expected.= 'aaaaaaaa';
+        $expected.= 'bbb';
+        $expected.= 'aaaaaaa';
+        $expected.= '
'; + + $this->assertSame($ansi->getScreen(), $expected); + } +}