From 82462ec57f1e9605a7fef9c4d1fedf1475b1ed67 Mon Sep 17 00:00:00 2001 From: gitlost Date: Mon, 24 Jul 2017 20:17:24 +0100 Subject: [PATCH 01/29] Various half-worked fixes relating to #106 and PR #109. --- lib/cli/Colors.php | 50 +++++++++++----------- lib/cli/Table.php | 28 ++++++++++++- lib/cli/cli.php | 30 ++++++++++---- lib/cli/table/Ascii.php | 40 ++++++++++++++---- tests/test-cli.php | 52 +++++++++++++++++++++-- tests/test-table.php | 92 ++++++++++++++++++++++++++++++++++++++++- 6 files changed, 245 insertions(+), 47 deletions(-) diff --git a/lib/cli/Colors.php b/lib/cli/Colors.php index 3efe8ed..6c0fb66 100644 --- a/lib/cli/Colors.php +++ b/lib/cli/Colors.php @@ -91,7 +91,7 @@ static public function color($color) { $colors = array(); foreach (array('color', 'style', 'background') as $type) { - $code = @$color[$type]; + $code = $color[$type]; if (isset(self::$_colors[$type][$code])) { $colors[] = self::$_colors[$type][$code]; } @@ -143,11 +143,16 @@ static public function colorize($string, $colored = null) { * Remove color information from a string. * * @param string $string A string with color information. + * @param bool $keep_tokens Optional. If set, color tokens (eg "%n") won't be stripped. Default false. * @return string A string with color information removed. */ - static public function decolorize($string) { - // Get rid of color tokens if they exist - $string = str_replace(array_keys(self::getColors()), '', $string); + static public function decolorize( $string, bool $keep_tokens = false ) { + if ( ! $keep_tokens ) { + // Get rid of color tokens if they exist + $string = str_replace('%%', '%¾', $string); + $string = str_replace(array_keys(self::getColors()), '', $string); + $string = str_replace('%¾', '%', $string); + } // Remove color encoding if it exists foreach (self::getColors() as $key => $value) { @@ -179,41 +184,34 @@ static public function cacheString($passed, $colorized, $colored) { * @return int */ static public function length($string) { - if (isset(self::$_string_cache[md5($string)]['decolorized'])) { - $test_string = self::$_string_cache[md5($string)]['decolorized']; - } else { - $test_string = self::decolorize($string); - } - - return safe_strlen($test_string); + return safe_strlen( self::decolorize( $string ) ); } /** - * Return the width (length in characters) of the string without color codes. + * Return the width (length in characters) of the string without color codes if enabled. * - * @param string $string the string to measure + * @param string $string The string to measure. + * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. * @return int */ - static public function width($string) { - $md5 = md5($string); - if (isset(self::$_string_cache[$md5]['decolorized'])) { - $test_string = self::$_string_cache[$md5]['decolorized']; - } else { - $test_string = self::decolorize($string); - } - - return strwidth($test_string); + static public function width( $string, bool $pre_colorized = false ) { + return strwidth( $pre_colorized || self::shouldColorize() ? self::decolorize( $string, $pre_colorized /*keep_tokens*/ ) : $string ); } /** * Pad the string to a certain display length. * - * @param string $string the string to pad - * @param integer $length the display length + * @param string $string The string to pad. + * @param int $length The display length. + * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. * @return string */ - static public function pad($string, $length) { - return safe_str_pad( $string, $length ); + static public function pad( $string, $length, bool $pre_colorized = false ) { + $real_length = self::width( $string, $pre_colorized ); + $diff = strlen( $string ) - $real_length; + $length += $diff; + + return str_pad( $string, $length ); } /** diff --git a/lib/cli/Table.php b/lib/cli/Table.php index ccbfa8f..f0d75b7 100644 --- a/lib/cli/Table.php +++ b/lib/cli/Table.php @@ -102,7 +102,7 @@ public function setRenderer(Renderer $renderer) { */ protected function checkRow(array $row) { foreach ($row as $column => $str) { - $width = Colors::shouldColorize() ? Colors::width($str) : strwidth($str); + $width = Colors::width( $str, $this->isAsciiPreColorized( $column ) ); if (!isset($this->_width[$column]) || $width > $this->_width[$column]) { $this->_width[$column] = $width; } @@ -228,4 +228,30 @@ public function setRows(array $rows) { public function countRows() { return count($this->_rows); } + + /** + * Set whether items in an Ascii table are pre-colorized. + * + * @param bool|array $precolorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. + * @see cli\Ascii::setPreColorized() + */ + public function setAsciiPreColorized( $pre_colorized ) { + if ( $this->_renderer instanceof Ascii ) { + $this->_renderer->setPreColorized( $pre_colorized ); + } + } + + /** + * Is a column in an Ascii table pre-colorized? + * + * @param int $column Column index to check. + * @return bool True if whole Ascii table is marked as pre-colorized, or if the individual column is pre-colorized; else false. + * @see cli\Ascii::isPreColorized() + */ + private function isAsciiPreColorized( int $column ) { + if ( $this->_renderer instanceof Ascii ) { + return $this->_renderer->isPreColorized( $column ); + } + return false; + } } diff --git a/lib/cli/cli.php b/lib/cli/cli.php index ebda153..6e03964 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -176,14 +176,29 @@ function safe_strlen( $str ) { * Attempts an encoding-safe way of getting a substring. If mb_string extensions aren't * installed, falls back to ascii substring if no encoding is present * - * @param string $str The input string - * @param int $start The starting position of the substring - * @param boolean $length Maximum length of the substring - * @return string Substring of string specified by start and length parameters + * @param string $str The input string. + * @param int $start The starting position of the substring. + * @param int|boolean $length Optional. Maximum length of the substring. Default false but should set to null for `substr()` compat behavior. + * @param boolean $width Optional. If set and encoding is UTF-8, $length is interpreted as spacing width. Default false. + * @return string Substring of string specified by start and length parameters */ -function safe_substr( $str, $start, $length = false ) { +function safe_substr( $str, $start, $length = false, $width = false ) { if ( function_exists( 'mb_substr' ) && function_exists( 'mb_detect_encoding' ) ) { - $substr = mb_substr( $str, $start, $length, mb_detect_encoding( $str ) ); + $encoding = mb_detect_encoding( $str ); + if ( false !== $width && 'UTF-8' === $encoding ) { + static $eaw_regex; // East Asian Width regex. Characters that count as 2 characters as they're "wide" or "fullwidth". See http://www.unicode.org/reports/tr11/tr11-19.html + if ( null === $eaw_regex ) { + // Load both regexs generated from Unicode data. + require __DIR__ . '/unicode/regex.php'; + } + $cnt = preg_match_all( '/[\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*/', $str, $matches ); + $width = $length; + + for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { + $width -= preg_match( $eaw_regex, $matches[0][ $length ] ) ? 2 : 1; + } + } + $substr = mb_substr( $str, $start, $length, $encoding ); } else { // iconv will return PHP notice if non-ascii characters are present in input string $str = iconv( 'ASCII' , 'ASCII', $str ); @@ -202,8 +217,7 @@ function safe_substr( $str, $start, $length = false ) { * @return string */ function safe_str_pad( $string, $length ) { - $cleaned_string = Colors::shouldColorize() ? Colors::decolorize( $string ) : $string; - $real_length = strwidth( $cleaned_string ); + $real_length = strwidth( $string ); $diff = strlen( $string ) - $real_length; $length += $diff; diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index 9d9bbf9..453702a 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -27,6 +27,7 @@ class Ascii extends Renderer { ); protected $_border = null; protected $_constraintWidth = null; + protected $_pre_colorized = false; /** * Set the widths of each column in the table. @@ -133,17 +134,17 @@ public function row( array $row ) { $value = str_replace( PHP_EOL, ' ', $value ); $col_width = $this->_widths[ $col ]; - $original_val_width = Colors::shouldColorize() ? Colors::width( $value ) : \cli\strwidth( $value ); + $original_val_width = Colors::width( $value, self::isPreColorized( $col ) ); if ( $original_val_width > $col_width ) { - $row[ $col ] = \cli\safe_substr( $value, 0, $col_width ); - $value = \cli\safe_substr( $value, $col_width, $original_val_width ); + $row[ $col ] = \cli\safe_substr( $value, 0, $col_width, true /*width*/ ); + $value = \cli\safe_substr( $value, \cli\safe_strlen( $row[ $col ] ), null ); $i = 0; do { - $extra_value = \cli\safe_substr( $value, 0, $col_width ); - $val_width = \cli\strwidth( $extra_value ); + $extra_value = \cli\safe_substr( $value, 0, $col_width, true /*width*/ ); + $val_width = Colors::width( $extra_value, self::isPreColorized( $col ) ); if ( $val_width ) { $extra_rows[ $col ][] = $extra_value; - $value = \cli\safe_substr( $value, $col_width, $original_val_width ); + $value = \cli\safe_substr( $value, \cli\safe_strlen( $extra_value ), null ); $i++; if ( $i > $extra_row_count ) { $extra_row_count = $i; @@ -188,6 +189,31 @@ public function row( array $row ) { } private function padColumn($content, $column) { - return $this->_characters['padding'] . Colors::pad($content, $this->_widths[$column]) . $this->_characters['padding']; + return $this->_characters['padding'] . Colors::pad( $content, $this->_widths[ $column ], $this->isPreColorized( $column ) ) . $this->_characters['padding']; + } + + /** + * Set whether items are pre-colorized. + * + * @param bool|array $colorized A boolean to set all columns in the table as pre-colorized, or an array of booleans keyed by column index (number) to set individual columns as pre-colorized. + */ + public function setPreColorized( $pre_colorized ) { + $this->_pre_colorized = $pre_colorized; + } + + /** + * Is a column pre-colorized? + * + * @param int $column Column index to check. + * @return bool True if whole table is marked as pre-colorized, or if the individual column is pre-colorized; else false. + */ + public function isPreColorized( int $column ) { + if ( is_bool( $this->_pre_colorized ) ) { + return $this->_pre_colorized; + } + if ( is_array( $this->_pre_colorized ) && isset( $this->_pre_colorized[ $column ] ) ) { + return $this->_pre_colorized[ $column ]; + } + return false; } } diff --git a/tests/test-cli.php b/tests/test-cli.php index 063c768..ce9a710 100644 --- a/tests/test-cli.php +++ b/tests/test-cli.php @@ -45,8 +45,30 @@ function test_encoded_string_pad() { } function test_colorized_string_pad() { - $this->assertEquals( 22, strlen( \cli\Colors::pad( \cli\Colors::colorize( "%Gx%n", true ), 11 ))); // colorized `x` string - $this->assertEquals( 23, strlen( \cli\Colors::pad( \cli\Colors::colorize( "%Góra%n", true ), 11 ))); // colorized `óra` string + // Colors enabled. + + $colorized = \cli\Colors::colorize( '%Gx%n', true ); // colorized `x` string + $this->assertSame( 22, strlen( \cli\Colors::pad( $colorized, 11 ) ) ); + $this->assertSame( 22, strlen( \cli\Colors::pad( $colorized, 11, false /*pre_colorized*/ ) ) ); + $this->assertSame( 22, strlen( \cli\Colors::pad( $colorized, 11, true /*pre_colorized*/ ) ) ); + + $colorized = \cli\Colors::colorize( "%Góra%n", true ); // colorized `óra` string + $this->assertSame( 23, strlen( \cli\Colors::pad( $colorized, 11 ) ) ); + $this->assertSame( 23, strlen( \cli\Colors::pad( $colorized, 11, false /*pre_colorized*/ ) ) ); + $this->assertSame( 23, strlen( \cli\Colors::pad( $colorized, 11, true /*pre_colorized*/ ) ) ); + + // Colors disabled. + \cli\Colors::disable( true ); + + $colorized = \cli\Colors::colorize( '%Gx%n', true ); // colorized `x` string + $this->assertSame( 12, strlen( \cli\Colors::pad( $colorized, 12 ) ) ); + $this->assertSame( 12, strlen( \cli\Colors::pad( $colorized, 12, false /*pre_colorized*/ ) ) ); + $this->assertSame( 23, strlen( \cli\Colors::pad( $colorized, 12, true /*pre_colorized*/ ) ) ); + + $colorized = \cli\Colors::colorize( "%Góra%n", true ); // colorized `óra` string + $this->assertSame( 16, strlen( \cli\Colors::pad( $colorized, 15 ) ) ); + $this->assertSame( 16, strlen( \cli\Colors::pad( $colorized, 15, false /*pre_colorized*/ ) ) ); + $this->assertSame( 27, strlen( \cli\Colors::pad( $colorized, 15, true /*pre_colorized*/ ) ) ); } function test_encoded_substr() { @@ -63,8 +85,30 @@ function test_colorized_string_length() { } function test_colorized_string_width() { - $this->assertEquals( \cli\Colors::width( \cli\Colors::colorize( '%Gx%n', true ) ), 1 ); - $this->assertEquals( \cli\Colors::width( \cli\Colors::colorize( '%G日%n', true ) ), 2 ); // Double-width char. + // Colors enabled. + + $colorized = \cli\Colors::colorize( '%Gx%n', true ); + $this->assertSame( 1, \cli\Colors::width( $colorized ) ); + $this->assertSame( 1, \cli\Colors::width( $colorized, false /*pre_colorized*/ ) ); + $this->assertSame( 1, \cli\Colors::width( $colorized, true /*pre_colorized*/ ) ); + + $colorized = \cli\Colors::colorize( '%G日%n', true ); // Double-width char. + $this->assertSame( 2, \cli\Colors::width( $colorized ) ); + $this->assertSame( 2, \cli\Colors::width( $colorized, false /*pre_colorized*/ ) ); + $this->assertSame( 2, \cli\Colors::width( $colorized, true /*pre_colorized*/ ) ); + + // Colors disabled. + \cli\Colors::disable( true ); + + $colorized = \cli\Colors::colorize( '%Gx%n', true ); + $this->assertSame( 12, \cli\Colors::width( $colorized ) ); + $this->assertSame( 12, \cli\Colors::width( $colorized, false /*pre_colorized*/ ) ); + $this->assertSame( 1, \cli\Colors::width( $colorized, true /*pre_colorized*/ ) ); + + $colorized = \cli\Colors::colorize( '%G日%n', true ); // Double-width char. + $this->assertSame( 13, \cli\Colors::width( $colorized ) ); + $this->assertSame( 13, \cli\Colors::width( $colorized, false /*pre_colorized*/ ) ); + $this->assertSame( 2, \cli\Colors::width( $colorized, true /*pre_colorized*/ ) ); } function test_colorize_string_is_colored() { diff --git a/tests/test-table.php b/tests/test-table.php index 01340f8..59d9569 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -1,5 +1,7 @@ setConstraintWidth( $constraint_width ); + $table->setRenderer( $renderer ); + $table->setHeaders( array( 'Field', 'Value' ) ); + $table->addRow( array( 'この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。この文章はダミーです。文字の大きさ、', 'こんにちは' ) ); + $table->addRow( array( 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', 'Hello' ) ); + + $out = $table->getDisplayLines(); + print_r( $out ); + for ( $i = 0; $i < count( $out ); $i++ ) { + $this->assertEquals( $constraint_width, mb_strwidth( $out[$i] ) + 1 ); + } + } + + public function test_ascii_pre_colorized_widths() { + + Colors::enable( true ); + + $headers = array( 'package', 'version', 'result' ); + $items = array( + array( Colors::colorize( '%ygaa/gaa-kabes%n' ), 'dev-master', Colors::colorize( "%rx%n" ) ), + array( Colors::colorize( '%ygaa/gaa-log%n' ), '*', Colors::colorize( "%gok%n" ) ), + array( Colors::colorize( '%ygaa/gaa-nonsense%n' ), 'v3.0.11', Colors::colorize( "%rx%n" ) ), + array( Colors::colorize( '%ygaa/gaa-100%%new%n' ), 'v100%new', Colors::colorize( "%gok%n" ) ), + ); + + // Disable colorization, as `\WP_CLI\Formatter::show_table()` does for Ascii tables. + Colors::disable( true ); + $this->assertFalse( Colors::shouldColorize() ); + + // Account for colorization of columns 0 & 2. + + $table = new Table; + $renderer = new Ascii; + $table->setRenderer( $renderer ); + $table->setAsciiPreColorized( array( true, false, true ) ); + $table->setHeaders( $headers ); + $table->setRows( $items ); + + $out = $table->getDisplayLines(); + error_log( "out=" . print_r( $out, true ) ); + + // "+ 4" accommodates 3 borders and header. + $this->assertSame( 4 + 4, count( $out ) ); + + // Borders & header. + $this->assertSame( 42, strlen( $out[0] ) ); + $this->assertSame( 42, strlen( $out[1] ) ); + $this->assertSame( 42, strlen( $out[2] ) ); + $this->assertSame( 42, strlen( $out[7] ) ); + + // Data. + $this->assertSame( 60, strlen( $out[3] ) ); + $this->assertSame( 60, strlen( $out[4] ) ); + $this->assertSame( 60, strlen( $out[5] ) ); + $this->assertSame( 60, strlen( $out[6] ) ); + + // Don't account for colorization of columns 0 & 2. + + $table = new Table; + $renderer = new Ascii; + $table->setRenderer( $renderer ); + $table->setHeaders( $headers ); + $table->setRows( $items ); + + $out = $table->getDisplayLines(); + + // "+ 4" accommodates 3 borders and header. + $this->assertSame( 4 + 4, count( $out ) ); + + // Borders & header. + $this->assertSame( 56, strlen( $out[0] ) ); + $this->assertSame( 56, strlen( $out[1] ) ); + $this->assertSame( 56, strlen( $out[2] ) ); + $this->assertSame( 56, strlen( $out[7] ) ); + + // Data. + $this->assertSame( 56, strlen( $out[3] ) ); + $this->assertSame( 56, strlen( $out[4] ) ); + $this->assertSame( 56, strlen( $out[5] ) ); + $this->assertSame( 56, strlen( $out[6] ) ); + } + +} From 7a7438df6d9d8f63ba6a5dd44f895794738c0ab0 Mon Sep 17 00:00:00 2001 From: gitlost Date: Mon, 24 Jul 2017 20:26:18 +0100 Subject: [PATCH 02/29] Remove bool hint on decolorize(). --- lib/cli/Colors.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli/Colors.php b/lib/cli/Colors.php index 6c0fb66..54081ee 100644 --- a/lib/cli/Colors.php +++ b/lib/cli/Colors.php @@ -146,7 +146,7 @@ static public function colorize($string, $colored = null) { * @param bool $keep_tokens Optional. If set, color tokens (eg "%n") won't be stripped. Default false. * @return string A string with color information removed. */ - static public function decolorize( $string, bool $keep_tokens = false ) { + static public function decolorize( $string, $keep_tokens = false ) { if ( ! $keep_tokens ) { // Get rid of color tokens if they exist $string = str_replace('%%', '%¾', $string); From a9e31e04449876ef54f0c50f03a365ff661e8767 Mon Sep 17 00:00:00 2001 From: gitlost Date: Mon, 24 Jul 2017 20:28:06 +0100 Subject: [PATCH 03/29] Sigh. Remove the other ones. --- lib/cli/Colors.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cli/Colors.php b/lib/cli/Colors.php index 54081ee..fa9d1ac 100644 --- a/lib/cli/Colors.php +++ b/lib/cli/Colors.php @@ -194,7 +194,7 @@ static public function length($string) { * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. * @return int */ - static public function width( $string, bool $pre_colorized = false ) { + static public function width( $string, $pre_colorized = false ) { return strwidth( $pre_colorized || self::shouldColorize() ? self::decolorize( $string, $pre_colorized /*keep_tokens*/ ) : $string ); } @@ -206,7 +206,7 @@ static public function width( $string, bool $pre_colorized = false ) { * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. * @return string */ - static public function pad( $string, $length, bool $pre_colorized = false ) { + static public function pad( $string, $length, $pre_colorized = false ) { $real_length = self::width( $string, $pre_colorized ); $diff = strlen( $string ) - $real_length; $length += $diff; From 4bd13bc1cf7963cbaf9b26e595b80fab91afb5ff Mon Sep 17 00:00:00 2001 From: gitlost Date: Mon, 24 Jul 2017 20:33:55 +0100 Subject: [PATCH 04/29] Double sigh. Remove all the other ones. --- lib/cli/Table.php | 2 +- lib/cli/table/Ascii.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cli/Table.php b/lib/cli/Table.php index f0d75b7..ee7f42a 100644 --- a/lib/cli/Table.php +++ b/lib/cli/Table.php @@ -248,7 +248,7 @@ public function setAsciiPreColorized( $pre_colorized ) { * @return bool True if whole Ascii table is marked as pre-colorized, or if the individual column is pre-colorized; else false. * @see cli\Ascii::isPreColorized() */ - private function isAsciiPreColorized( int $column ) { + private function isAsciiPreColorized( $column ) { if ( $this->_renderer instanceof Ascii ) { return $this->_renderer->isPreColorized( $column ); } diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index 453702a..503eaa7 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -207,7 +207,7 @@ public function setPreColorized( $pre_colorized ) { * @param int $column Column index to check. * @return bool True if whole table is marked as pre-colorized, or if the individual column is pre-colorized; else false. */ - public function isPreColorized( int $column ) { + public function isPreColorized( $column ) { if ( is_bool( $this->_pre_colorized ) ) { return $this->_pre_colorized; } From 57befc8ca5d40f5f6eb68a290b174ef17e61d7de Mon Sep 17 00:00:00 2001 From: gitlost Date: Mon, 24 Jul 2017 20:55:02 +0100 Subject: [PATCH 05/29] Use strwidth() in test_column_value_too_long_with_multibytes. --- tests/test-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-table.php b/tests/test-table.php index 59d9569..de9ed10 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -52,7 +52,7 @@ public function test_column_value_too_long_with_multibytes() { $out = $table->getDisplayLines(); print_r( $out ); for ( $i = 0; $i < count( $out ); $i++ ) { - $this->assertEquals( $constraint_width, mb_strwidth( $out[$i] ) + 1 ); + $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) + 1 ); } } From 79a75c98cbc2c239e9e462d1a060adcb475209d3 Mon Sep 17 00:00:00 2001 From: gitlost Date: Mon, 24 Jul 2017 21:23:41 +0100 Subject: [PATCH 06/29] Set length to safe_strlen() if null for cross PHP compat. Explicitly enable Colors in tests. --- lib/cli/cli.php | 14 ++++++++++---- tests/test-cli.php | 2 ++ tests/test-table.php | 1 - 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/lib/cli/cli.php b/lib/cli/cli.php index 6e03964..5a9bb4f 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -183,6 +183,10 @@ function safe_strlen( $str ) { * @return string Substring of string specified by start and length parameters */ function safe_substr( $str, $start, $length = false, $width = false ) { + // PHP 5.3 substr takes false as full length, PHP > 5.3 takes null - for compat. do strlen. + if ( null === $length || false === $length ) { + $length = safe_strlen( $str ); + } if ( function_exists( 'mb_substr' ) && function_exists( 'mb_detect_encoding' ) ) { $encoding = mb_detect_encoding( $str ); if ( false !== $width && 'UTF-8' === $encoding ) { @@ -191,11 +195,13 @@ function safe_substr( $str, $start, $length = false, $width = false ) { // Load both regexs generated from Unicode data. require __DIR__ . '/unicode/regex.php'; } - $cnt = preg_match_all( '/[\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*/', $str, $matches ); - $width = $length; + if ( preg_match( $eaw_regex, $str ) ) { + $cnt = preg_match_all( '/[\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*/', $str, $matches ); + $width = $length; - for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { - $width -= preg_match( $eaw_regex, $matches[0][ $length ] ) ? 2 : 1; + for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { + $width -= preg_match( $eaw_regex, $matches[0][ $length ] ) ? 2 : 1; + } } } $substr = mb_substr( $str, $start, $length, $encoding ); diff --git a/tests/test-cli.php b/tests/test-cli.php index ce9a710..5f8def7 100644 --- a/tests/test-cli.php +++ b/tests/test-cli.php @@ -46,6 +46,7 @@ function test_encoded_string_pad() { function test_colorized_string_pad() { // Colors enabled. + \cli\Colors::enable( true ); $colorized = \cli\Colors::colorize( '%Gx%n', true ); // colorized `x` string $this->assertSame( 22, strlen( \cli\Colors::pad( $colorized, 11 ) ) ); @@ -86,6 +87,7 @@ function test_colorized_string_length() { function test_colorized_string_width() { // Colors enabled. + \cli\Colors::enable( true ); $colorized = \cli\Colors::colorize( '%Gx%n', true ); $this->assertSame( 1, \cli\Colors::width( $colorized ) ); diff --git a/tests/test-table.php b/tests/test-table.php index de9ed10..47a9670 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -82,7 +82,6 @@ public function test_ascii_pre_colorized_widths() { $table->setRows( $items ); $out = $table->getDisplayLines(); - error_log( "out=" . print_r( $out, true ) ); // "+ 4" accommodates 3 borders and header. $this->assertSame( 4 + 4, count( $out ) ); From 62fdd75ed47c9b4def714deb0d3d110156d01420 Mon Sep 17 00:00:00 2001 From: gitlost Date: Mon, 24 Jul 2017 22:26:12 +0100 Subject: [PATCH 07/29] Refactor the unicode regexs into a function. --- lib/cli/cli.php | 44 ++++++++++++++++++++++++++++++++------------ 1 file changed, 32 insertions(+), 12 deletions(-) diff --git a/lib/cli/cli.php b/lib/cli/cli.php index 5a9bb4f..5ce8f00 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -190,17 +190,15 @@ function safe_substr( $str, $start, $length = false, $width = false ) { if ( function_exists( 'mb_substr' ) && function_exists( 'mb_detect_encoding' ) ) { $encoding = mb_detect_encoding( $str ); if ( false !== $width && 'UTF-8' === $encoding ) { - static $eaw_regex; // East Asian Width regex. Characters that count as 2 characters as they're "wide" or "fullwidth". See http://www.unicode.org/reports/tr11/tr11-19.html - if ( null === $eaw_regex ) { - // Load both regexs generated from Unicode data. - require __DIR__ . '/unicode/regex.php'; - } + // Set the East Asian Width regex. + $eaw_regex = get_unicode_regexs( 'eaw' ); if ( preg_match( $eaw_regex, $str ) ) { $cnt = preg_match_all( '/[\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*/', $str, $matches ); + $chrs = $matches[0]; $width = $length; for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { - $width -= preg_match( $eaw_regex, $matches[0][ $length ] ) ? 2 : 1; + $width -= preg_match( $eaw_regex, $chrs[ $length ] ) ? 2 : 1; } } } @@ -237,12 +235,8 @@ function safe_str_pad( $string, $length ) { * @return int The string's width. */ function strwidth( $string ) { - static $eaw_regex; // East Asian Width regex. Characters that count as 2 characters as they're "wide" or "fullwidth". See http://www.unicode.org/reports/tr11/tr11-19.html - static $m_regex; // Mark characters regex (Unicode property "M") - mark combining "Mc", mark enclosing "Me" and mark non-spacing "Mn" chars that should be ignored for spacing purposes. - if ( null === $eaw_regex ) { - // Load both regexs generated from Unicode data. - require __DIR__ . '/unicode/regex.php'; - } + // Set the East Asian Width and Mark regexs. + list( $eaw_regex, $m_regex ) = get_unicode_regexs(); // Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strwidth(), "other" safe_strlen(). $test_strwidth = getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); @@ -272,3 +266,29 @@ function strwidth( $string ) { } return safe_strlen( $string ); } + +/** + * Get the regexs generated from Unicode data. + * + * @param string $idx Optional. Return a specific regex only. Default null. + * @return array|string Returns keyed array if not given $idx or $idx doesn't exist, otherwise the specific regex string. + */ +function get_unicode_regexs( $idx = null ) { + static $eaw_regex; // East Asian Width regex. Characters that count as 2 characters as they're "wide" or "fullwidth". See http://www.unicode.org/reports/tr11/tr11-19.html + static $m_regex; // Mark characters regex (Unicode property "M") - mark combining "Mc", mark enclosing "Me" and mark non-spacing "Mn" chars that should be ignored for spacing purposes. + if ( null === $eaw_regex ) { + // Load both regexs generated from Unicode data. + require __DIR__ . '/unicode/regex.php'; + } + + if ( null !== $idx ) { + if ( 'eaw' === $idx ) { + return $eaw_regex; + } + if ( 'm' === $idx ) { + return $m_regex; + } + } + + return array( $eaw_regex, $m_regex, ); +} From 7466f68a76603d9a1734616aa30fc883724d58cc Mon Sep 17 00:00:00 2001 From: gitlost Date: Tue, 25 Jul 2017 10:10:02 +0100 Subject: [PATCH 08/29] Copy core _mb_substr. Add encoding arg. Add keep arg to decolorize. --- lib/cli/Colors.php | 57 +++++++------ lib/cli/cli.php | 81 ++++++++++-------- lib/cli/table/Ascii.php | 13 +-- tests/test-cli.php | 177 ++++++++++++++++++++++++++++++++-------- tests/test-table.php | 1 - 5 files changed, 226 insertions(+), 103 deletions(-) diff --git a/lib/cli/Colors.php b/lib/cli/Colors.php index fa9d1ac..e764ec8 100644 --- a/lib/cli/Colors.php +++ b/lib/cli/Colors.php @@ -115,18 +115,17 @@ static public function color($color) { static public function colorize($string, $colored = null) { $passed = $string; - if (isset(self::$_string_cache[md5($passed)]['colorized'])) { - return self::$_string_cache[md5($passed)]['colorized']; - } - if (!self::shouldColorize($colored)) { - $colors = self::getColors(); - $search = array_keys( $colors ); - $return = str_replace( $search, '', $string ); - self::cacheString($passed, $return, $colored); + $return = self::decolorize( $passed, 2 /*keep_encodings*/ ); + self::cacheString($passed, $return); return $return; } + $md5 = md5($passed); + if (isset(self::$_string_cache[$md5]['colorized'])) { + return self::$_string_cache[$md5]['colorized']; + } + $string = str_replace('%%', '%¾', $string); foreach (self::getColors() as $key => $value) { @@ -134,7 +133,7 @@ static public function colorize($string, $colored = null) { } $string = str_replace('%¾', '%', $string); - self::cacheString($passed, $string, $colored); + self::cacheString($passed, $string); return $string; } @@ -143,20 +142,22 @@ static public function colorize($string, $colored = null) { * Remove color information from a string. * * @param string $string A string with color information. - * @param bool $keep_tokens Optional. If set, color tokens (eg "%n") won't be stripped. Default false. + * @param int $keep Optional. If the 1 bit is set, color tokens (eg "%n") won't be stripped. If the 2 bit is set, color encodings (ANSI escapes) won't be stripped. Default 0. * @return string A string with color information removed. */ - static public function decolorize( $string, $keep_tokens = false ) { - if ( ! $keep_tokens ) { + static public function decolorize( $string, $keep = 0 ) { + if ( ! ( $keep & 1 ) ) { // Get rid of color tokens if they exist $string = str_replace('%%', '%¾', $string); $string = str_replace(array_keys(self::getColors()), '', $string); $string = str_replace('%¾', '%', $string); } - // Remove color encoding if it exists - foreach (self::getColors() as $key => $value) { - $string = str_replace(self::color($value), '', $string); + if ( ! ( $keep & 2 ) ) { + // Remove color encoding if it exists + foreach (self::getColors() as $key => $value) { + $string = str_replace(self::color($value), '', $string); + } } return $string; @@ -167,13 +168,13 @@ static public function decolorize( $string, $keep_tokens = false ) { * * @param string $passed The original string before colorization. * @param string $colorized The string after running through self::colorize. - * @param string $colored The string without any color information. + * @param string $deprecated Optional. Not used. Default null. */ - static public function cacheString($passed, $colorized, $colored) { + static public function cacheString( $passed, $colorized, $deprecated = null ) { self::$_string_cache[md5($passed)] = array( 'passed' => $passed, 'colorized' => $colorized, - 'decolorized' => self::decolorize($passed) + 'decolorized' => self::decolorize($passed), // Not very useful but keep for BC. ); } @@ -190,24 +191,26 @@ static public function length($string) { /** * Return the width (length in characters) of the string without color codes if enabled. * - * @param string $string The string to measure. - * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. + * @param string $string The string to measure. + * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. + * @param string|bool $encoding Optional. The encoding of the string. Default false. * @return int */ - static public function width( $string, $pre_colorized = false ) { - return strwidth( $pre_colorized || self::shouldColorize() ? self::decolorize( $string, $pre_colorized /*keep_tokens*/ ) : $string ); + static public function width( $string, $pre_colorized = false, $encoding = false ) { + return strwidth( $pre_colorized || self::shouldColorize() ? self::decolorize( $string, $pre_colorized ? 1 /*keep_tokens*/ : 0 ) : $string, $encoding ); } /** * Pad the string to a certain display length. * - * @param string $string The string to pad. - * @param int $length The display length. - * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. + * @param string $string The string to pad. + * @param int $length The display length. + * @param bool $pre_colorized Optional. Set if the string is pre-colorized. Default false. + * @param string|bool $encoding Optional. The encoding of the string. Default false. * @return string */ - static public function pad( $string, $length, $pre_colorized = false ) { - $real_length = self::width( $string, $pre_colorized ); + static public function pad( $string, $length, $pre_colorized = false, $encoding = false ) { + $real_length = self::width( $string, $pre_colorized, $encoding ); $diff = strlen( $string ) - $real_length; $length += $diff; diff --git a/lib/cli/cli.php b/lib/cli/cli.php index 5ce8f00..0c67346 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -156,15 +156,19 @@ function menu( $items, $default = null, $title = 'Choose an item' ) { * Attempts an encoding-safe way of getting string length. If mb_string extensions aren't * installed, falls back to basic strlen if no encoding is present * - * @param string The string to check - * @return int Numeric value that represents the string's length + * @param string $str The string to check. + * @param string|bool $encoding Optional. The encoding of the string. Default false. + * @return int Numeric value that represents the string's length */ -function safe_strlen( $str ) { - if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_encoding' ) ) { - $length = mb_strlen( $str, mb_detect_encoding( $str ) ); +function safe_strlen( $str, $encoding = false ) { + if ( function_exists( 'mb_strlen' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { + if ( ! $encoding ) { + $encoding = mb_detect_encoding( $str, null, true /*strict*/ ); + } + $length = mb_strlen( $str, $encoding ); } else { // iconv will return PHP notice if non-ascii characters are present in input string - $str = iconv( 'ASCII' , 'ASCII', $str ); + $str = iconv( $encoding ? $encoding : 'ASCII', 'ASCII', $str ); $length = strlen( $str ); } @@ -176,36 +180,43 @@ function safe_strlen( $str ) { * Attempts an encoding-safe way of getting a substring. If mb_string extensions aren't * installed, falls back to ascii substring if no encoding is present * - * @param string $str The input string. - * @param int $start The starting position of the substring. - * @param int|boolean $length Optional. Maximum length of the substring. Default false but should set to null for `substr()` compat behavior. - * @param boolean $width Optional. If set and encoding is UTF-8, $length is interpreted as spacing width. Default false. - * @return string Substring of string specified by start and length parameters + * @param string $str The input string. + * @param int $start The starting position of the substring. + * @param int|bool|null $length Optional. Maximum length of the substring. Default false. + * @param int|bool $is_width Optional. If set and encoding is UTF-8, $length is interpreted as spacing width. Default false. + * @param string|bool $encoding Optional. The encoding of the string. Default false. + * @return string Substring of string specified by start and length parameters */ -function safe_substr( $str, $start, $length = false, $width = false ) { - // PHP 5.3 substr takes false as full length, PHP > 5.3 takes null - for compat. do strlen. +function safe_substr( $str, $start, $length = false, $is_width = false, $encoding = false ) { + // PHP 5.3 substr takes false as full length, PHP > 5.3 takes null - for compat. do `safe_strlen()`. if ( null === $length || false === $length ) { - $length = safe_strlen( $str ); + $length = safe_strlen( $str, $encoding ); } - if ( function_exists( 'mb_substr' ) && function_exists( 'mb_detect_encoding' ) ) { - $encoding = mb_detect_encoding( $str ); - if ( false !== $width && 'UTF-8' === $encoding ) { + if ( function_exists( 'mb_substr' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { + if ( ! $encoding ) { + $encoding = mb_detect_encoding( $str, null, true /*strict*/ ); + } + $substr = mb_substr( $str, $start, $length, $encoding ); + + if ( $is_width && 'UTF-8' === $encoding ) { // Set the East Asian Width regex. $eaw_regex = get_unicode_regexs( 'eaw' ); - if ( preg_match( $eaw_regex, $str ) ) { - $cnt = preg_match_all( '/[\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*/', $str, $matches ); - $chrs = $matches[0]; + // If there's any East Asian double-width chars... + if ( preg_match( $eaw_regex, $substr ) ) { + // Explode string into an array of UTF-8 chars. Based on core `_mb_substr()` in "wp-includes/compat.php". + $chars = preg_split( '/([\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*)/', $substr, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); + $cnt = min( count( $chars ), $length ); $width = $length; for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { - $width -= preg_match( $eaw_regex, $chrs[ $length ] ) ? 2 : 1; + $width -= preg_match( $eaw_regex, $chars[ $length ] ) ? 2 : 1; } + return join( '', array_slice( $chars, 0, $length ) ); } } - $substr = mb_substr( $str, $start, $length, $encoding ); } else { // iconv will return PHP notice if non-ascii characters are present in input string - $str = iconv( 'ASCII' , 'ASCII', $str ); + $str = iconv( $encoding ? $encoding : 'ASCII', 'ASCII', $str ); $substr = substr( $str, $start, $length ); } @@ -216,12 +227,13 @@ function safe_substr( $str, $start, $length = false, $width = false ) { /** * An encoding-safe way of padding string length for display * - * @param string $string The string to pad - * @param int $length The length to pad it to + * @param string $string The string to pad. + * @param int $length The length to pad it to. + * @param string|bool $encoding Optional. The encoding of the string. Default false. * @return string */ -function safe_str_pad( $string, $length ) { - $real_length = strwidth( $string ); +function safe_str_pad( $string, $length, $encoding = false ) { + $real_length = strwidth( $string, $encoding ); $diff = strlen( $string ) - $real_length; $length += $diff; @@ -231,10 +243,11 @@ function safe_str_pad( $string, $length ) { /** * Get width of string, ie length in characters, taking into account multi-byte and mark characters for UTF-8, and multi-byte for non-UTF-8. * - * @param string The string to check - * @return int The string's width. + * @param string $string The string to check. + * @param string|bool $encoding Optional. The encoding of the string. Default false. + * @return int The string's width. */ -function strwidth( $string ) { +function strwidth( $string, $encoding = false ) { // Set the East Asian Width and Mark regexs. list( $eaw_regex, $m_regex ) = get_unicode_regexs(); @@ -253,8 +266,10 @@ function strwidth( $string ) { return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ ); } } - if ( function_exists( 'mb_strwidth' ) && function_exists( 'mb_detect_encoding' ) ) { - $encoding = mb_detect_encoding( $string, null, true /*strict*/ ); + if ( function_exists( 'mb_strwidth' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { + if ( ! $encoding ) { + $encoding = mb_detect_encoding( $string, null, true /*strict*/ ); + } $width = mb_strwidth( $string, $encoding ); if ( 'UTF-8' === $encoding ) { // Subtract combining characters. @@ -271,7 +286,7 @@ function strwidth( $string ) { * Get the regexs generated from Unicode data. * * @param string $idx Optional. Return a specific regex only. Default null. - * @return array|string Returns keyed array if not given $idx or $idx doesn't exist, otherwise the specific regex string. + * @return array|string Returns keyed array if not given $idx or $idx doesn't exist, otherwise the specific regex string. */ function get_unicode_regexs( $idx = null ) { static $eaw_regex; // East Asian Width regex. Characters that count as 2 characters as they're "wide" or "fullwidth". See http://www.unicode.org/reports/tr11/tr11-19.html diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index 503eaa7..3372a9b 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -134,17 +134,18 @@ public function row( array $row ) { $value = str_replace( PHP_EOL, ' ', $value ); $col_width = $this->_widths[ $col ]; - $original_val_width = Colors::width( $value, self::isPreColorized( $col ) ); + $encoding = function_exists( 'mb_detect_encoding' ) ? mb_detect_encoding( $value, null, true /*strict*/ ) : false; + $original_val_width = Colors::width( $value, self::isPreColorized( $col ), $encoding ); if ( $original_val_width > $col_width ) { - $row[ $col ] = \cli\safe_substr( $value, 0, $col_width, true /*width*/ ); - $value = \cli\safe_substr( $value, \cli\safe_strlen( $row[ $col ] ), null ); + $row[ $col ] = \cli\safe_substr( $value, 0, $col_width, true /*is_width*/, $encoding ); + $value = \cli\safe_substr( $value, \cli\safe_strlen( $row[ $col ], $encoding ), null /*length*/, false /*is_width*/, $encoding ); $i = 0; do { - $extra_value = \cli\safe_substr( $value, 0, $col_width, true /*width*/ ); - $val_width = Colors::width( $extra_value, self::isPreColorized( $col ) ); + $extra_value = \cli\safe_substr( $value, 0, $col_width, true /*is_width*/, $encoding ); + $val_width = Colors::width( $extra_value, self::isPreColorized( $col ), $encoding ); if ( $val_width ) { $extra_rows[ $col ][] = $extra_value; - $value = \cli\safe_substr( $value, \cli\safe_strlen( $extra_value ), null ); + $value = \cli\safe_substr( $value, \cli\safe_strlen( $extra_value, $encoding ), null /*length*/, false /*is_width*/, $encoding ); $i++; if ( $i > $extra_row_count ) { $extra_row_count = $i; diff --git a/tests/test-cli.php b/tests/test-cli.php index 5f8def7..6ab4a0c 100644 --- a/tests/test-cli.php +++ b/tests/test-cli.php @@ -1,5 +1,7 @@ assertSame( 22, strlen( \cli\Colors::pad( $colorized, 11 ) ) ); - $this->assertSame( 22, strlen( \cli\Colors::pad( $colorized, 11, false /*pre_colorized*/ ) ) ); - $this->assertSame( 22, strlen( \cli\Colors::pad( $colorized, 11, true /*pre_colorized*/ ) ) ); + $this->assertSame( 22, strlen( Colors::pad( $x, 11 ) ) ); + $this->assertSame( 22, strlen( Colors::pad( $x, 11, false /*pre_colorized*/ ) ) ); + $this->assertSame( 22, strlen( Colors::pad( $x, 11, true /*pre_colorized*/ ) ) ); - $colorized = \cli\Colors::colorize( "%Góra%n", true ); // colorized `óra` string - $this->assertSame( 23, strlen( \cli\Colors::pad( $colorized, 11 ) ) ); - $this->assertSame( 23, strlen( \cli\Colors::pad( $colorized, 11, false /*pre_colorized*/ ) ) ); - $this->assertSame( 23, strlen( \cli\Colors::pad( $colorized, 11, true /*pre_colorized*/ ) ) ); + $this->assertSame( 23, strlen( Colors::pad( $ora, 11 ) ) ); // +1 for two-byte "ó". + $this->assertSame( 23, strlen( Colors::pad( $ora, 11, false /*pre_colorized*/ ) ) ); + $this->assertSame( 23, strlen( Colors::pad( $ora, 11, true /*pre_colorized*/ ) ) ); // Colors disabled. - \cli\Colors::disable( true ); + Colors::disable( true ); + $this->assertFalse( Colors::shouldColorize() ); - $colorized = \cli\Colors::colorize( '%Gx%n', true ); // colorized `x` string - $this->assertSame( 12, strlen( \cli\Colors::pad( $colorized, 12 ) ) ); - $this->assertSame( 12, strlen( \cli\Colors::pad( $colorized, 12, false /*pre_colorized*/ ) ) ); - $this->assertSame( 23, strlen( \cli\Colors::pad( $colorized, 12, true /*pre_colorized*/ ) ) ); + $this->assertSame( 20, strlen( Colors::pad( $x, 20 ) ) ); + $this->assertSame( 20, strlen( Colors::pad( $x, 20, false /*pre_colorized*/ ) ) ); + $this->assertSame( 31, strlen( Colors::pad( $x, 20, true /*pre_colorized*/ ) ) ); - $colorized = \cli\Colors::colorize( "%Góra%n", true ); // colorized `óra` string - $this->assertSame( 16, strlen( \cli\Colors::pad( $colorized, 15 ) ) ); - $this->assertSame( 16, strlen( \cli\Colors::pad( $colorized, 15, false /*pre_colorized*/ ) ) ); - $this->assertSame( 27, strlen( \cli\Colors::pad( $colorized, 15, true /*pre_colorized*/ ) ) ); + $this->assertSame( 21, strlen( Colors::pad( $ora, 20 ) ) ); // +1 for two-byte "ó". + $this->assertSame( 21, strlen( Colors::pad( $ora, 20, false /*pre_colorized*/ ) ) ); + $this->assertSame( 32, strlen( Colors::pad( $ora, 20, true /*pre_colorized*/ ) ) ); } function test_encoded_substr() { @@ -78,6 +80,57 @@ function test_encoded_substr() { $this->assertEquals( \cli\safe_substr( \cli\Colors::pad( 'óra', 6), 0, 2 ), 'ór' ); $this->assertEquals( \cli\safe_substr( \cli\Colors::pad( '日本語', 6), 0, 2 ), '日本' ); + $this->assertSame( 'el', \cli\safe_substr( Colors::pad( 'hello', 6 ), 1, 2 ) ); + + $this->assertSame( 'a ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 2, 2 ) ); + $this->assertSame( ' ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 5, 2 ) ); + + $this->assertSame( '本語', \cli\safe_substr( Colors::pad( '日本語', 8 ), 1, 2 ) ); + $this->assertSame( '語 ', \cli\safe_substr( Colors::pad( '日本語', 8 ), 2, 2 ) ); + + } + + function test_encoded_substr_is_width() { + + $this->assertSame( 'he', \cli\safe_substr( Colors::pad( 'hello', 6 ), 0, 2, true /*is_width*/ ) ); + $this->assertSame( 'ór', \cli\safe_substr( Colors::pad( 'óra', 6 ), 0, 2, true /*is_width*/ ) ); + $this->assertSame( '日', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 2, true /*is_width*/ ) ); + $this->assertSame( '日本', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 4, true /*is_width*/ ) ); + $this->assertSame( '日本', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 3, true /*is_width*/ ) ); + $this->assertSame( '日本語', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 6, true /*is_width*/ ) ); + $this->assertSame( '日本語 ', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 7, true /*is_width*/ ) ); + + $this->assertSame( 'el', \cli\safe_substr( Colors::pad( 'hello', 6 ), 1, 2, true /*is_width*/ ) ); + + $this->assertSame( 'a ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 2, 2, true /*is_width*/ ) ); + $this->assertSame( ' ', \cli\safe_substr( Colors::pad( 'óra', 6 ), 5, 2, true /*is_width*/ ) ); + + $this->assertSame( '', \cli\safe_substr( '1日4本語90', 0, 0, true /*is_width*/ ) ); + $this->assertSame( '1', \cli\safe_substr( '1日4本語90', 0, 1, true /*is_width*/ ) ); + $this->assertSame( '1日', \cli\safe_substr( '1日4本語90', 0, 2, true /*is_width*/ ) ); + $this->assertSame( '1日', \cli\safe_substr( '1日4本語90', 0, 3, true /*is_width*/ ) ); + $this->assertSame( '1日4', \cli\safe_substr( '1日4本語90', 0, 4, true /*is_width*/ ) ); + $this->assertSame( '1日4本', \cli\safe_substr( '1日4本語90', 0, 5, true /*is_width*/ ) ); + $this->assertSame( '1日4本', \cli\safe_substr( '1日4本語90', 0, 6, true /*is_width*/ ) ); + $this->assertSame( '1日4本語', \cli\safe_substr( '1日4本語90', 0, 7, true /*is_width*/ ) ); + $this->assertSame( '1日4本語', \cli\safe_substr( '1日4本語90', 0, 8, true /*is_width*/ ) ); + $this->assertSame( '1日4本語9', \cli\safe_substr( '1日4本語90', 0, 9, true /*is_width*/ ) ); + $this->assertSame( '1日4本語90', \cli\safe_substr( '1日4本語90', 0, 10, true /*is_width*/ ) ); + $this->assertSame( '1日4本語90', \cli\safe_substr( '1日4本語90', 0, 11, true /*is_width*/ ) ); + + $this->assertSame( '日', \cli\safe_substr( '1日4本語90', 1, 2, true /*is_width*/ ) ); + $this->assertSame( '日4', \cli\safe_substr( '1日4本語90', 1, 3, true /*is_width*/ ) ); + $this->assertSame( '4本語9', \cli\safe_substr( '1日4本語90', 2, 6, true /*is_width*/ ) ); + + $this->assertSame( '本', \cli\safe_substr( '1日4本語90', 3, 1, true /*is_width*/ ) ); + $this->assertSame( '本', \cli\safe_substr( '1日4本語90', 3, 2, true /*is_width*/ ) ); + $this->assertSame( '本語', \cli\safe_substr( '1日4本語90', 3, 3, true /*is_width*/ ) ); + $this->assertSame( '本語', \cli\safe_substr( '1日4本語90', 3, 4, true /*is_width*/ ) ); + $this->assertSame( '本語9', \cli\safe_substr( '1日4本語90', 3, 5, true /*is_width*/ ) ); + + $this->assertSame( '0', \cli\safe_substr( '1日4本語90', 6, 1, true /*is_width*/ ) ); + $this->assertSame( '', \cli\safe_substr( '1日4本語90', 7, 1, true /*is_width*/ ) ); + $this->assertSame( '', \cli\safe_substr( '1日4本語90', 6, 0, true /*is_width*/ ) ); } function test_colorized_string_length() { @@ -87,30 +140,30 @@ function test_colorized_string_length() { function test_colorized_string_width() { // Colors enabled. - \cli\Colors::enable( true ); + Colors::enable( true ); + + $x = Colors::colorize( '%Gx%n', true ); + $dw = Colors::colorize( '%G日%n', true ); // Double-width char. - $colorized = \cli\Colors::colorize( '%Gx%n', true ); - $this->assertSame( 1, \cli\Colors::width( $colorized ) ); - $this->assertSame( 1, \cli\Colors::width( $colorized, false /*pre_colorized*/ ) ); - $this->assertSame( 1, \cli\Colors::width( $colorized, true /*pre_colorized*/ ) ); + $this->assertSame( 1, Colors::width( $x ) ); + $this->assertSame( 1, Colors::width( $x, false /*pre_colorized*/ ) ); + $this->assertSame( 1, Colors::width( $x, true /*pre_colorized*/ ) ); - $colorized = \cli\Colors::colorize( '%G日%n', true ); // Double-width char. - $this->assertSame( 2, \cli\Colors::width( $colorized ) ); - $this->assertSame( 2, \cli\Colors::width( $colorized, false /*pre_colorized*/ ) ); - $this->assertSame( 2, \cli\Colors::width( $colorized, true /*pre_colorized*/ ) ); + $this->assertSame( 2, Colors::width( $dw ) ); + $this->assertSame( 2, Colors::width( $dw, false /*pre_colorized*/ ) ); + $this->assertSame( 2, Colors::width( $dw, true /*pre_colorized*/ ) ); // Colors disabled. - \cli\Colors::disable( true ); + Colors::disable( true ); + $this->assertFalse( Colors::shouldColorize() ); - $colorized = \cli\Colors::colorize( '%Gx%n', true ); - $this->assertSame( 12, \cli\Colors::width( $colorized ) ); - $this->assertSame( 12, \cli\Colors::width( $colorized, false /*pre_colorized*/ ) ); - $this->assertSame( 1, \cli\Colors::width( $colorized, true /*pre_colorized*/ ) ); + $this->assertSame( 12, Colors::width( $x ) ); + $this->assertSame( 12, Colors::width( $x, false /*pre_colorized*/ ) ); + $this->assertSame( 1, Colors::width( $x, true /*pre_colorized*/ ) ); - $colorized = \cli\Colors::colorize( '%G日%n', true ); // Double-width char. - $this->assertSame( 13, \cli\Colors::width( $colorized ) ); - $this->assertSame( 13, \cli\Colors::width( $colorized, false /*pre_colorized*/ ) ); - $this->assertSame( 2, \cli\Colors::width( $colorized, true /*pre_colorized*/ ) ); + $this->assertSame( 13, Colors::width( $dw ) ); + $this->assertSame( 13, Colors::width( $dw, false /*pre_colorized*/ ) ); + $this->assertSame( 2, Colors::width( $dw, true /*pre_colorized*/ ) ); } function test_colorize_string_is_colored() { @@ -162,6 +215,58 @@ function test_string_cache() { $this->assertEquals( $test_cache, $real_cache[ md5( $string_with_color ) ] ); } + function test_string_cache_colorize() { + $string = 'x'; + $string_with_color = '%k' . $string; + $colorized_string = "\033[30m$string"; + + // Colors enabled. + Colors::enable( true ); + + // Ensure colorization works + $this->assertSame( $colorized_string, Colors::colorize( $string_with_color ) ); + $this->assertSame( $colorized_string, Colors::colorize( $string_with_color ) ); + + // Colors disabled. + Colors::disable( true ); + $this->assertFalse( Colors::shouldColorize() ); + + // Ensure it doesn't come from the cache. + $this->assertSame( $string, Colors::colorize( $string_with_color ) ); + $this->assertSame( $string, Colors::colorize( $string_with_color ) ); + + // Check that escaped % isn't stripped on putting into cache. + $string = 'x%%n'; + $string_with_color = '%k' . $string; + $this->assertSame( 'x%n', Colors::colorize( $string_with_color ) ); + $this->assertSame( 'x%n', Colors::colorize( $string_with_color ) ); + } + + function test_decolorize() { + // Colors enabled. + Colors::enable( true ); + + $string = '%kx%%n%n'; + $colorized_string = Colors::colorize( $string ); + $both_string = '%gfoo' . $colorized_string . 'bar%%%n'; + + $this->assertSame( 'x%n', Colors::decolorize( $string ) ); + $this->assertSame( 'x', Colors::decolorize( $colorized_string ) ); + $this->assertSame( 'fooxbar%', Colors::decolorize( $both_string ) ); + + $this->assertSame( $string, Colors::decolorize( $string, 1 /*keep_tokens*/ ) ); + $this->assertSame( 'x%n', Colors::decolorize( $colorized_string, 1 /*keep_tokens*/ ) ); + $this->assertSame( '%gfoox%nbar%%%n', Colors::decolorize( $both_string, 1 /*keep_tokens*/ ) ); + + $this->assertSame( 'x%n', Colors::decolorize( $string, 2 /*keep_encodings*/ ) ); + $this->assertSame( 'x', Colors::decolorize( $colorized_string, 2 /*keep_encodings*/ ) ); + $this->assertSame( 'fooxbar%', Colors::decolorize( $both_string, 2 /*keep_encodings*/ ) ); + + $this->assertSame( $string, Colors::decolorize( $string, 3 /*noop*/ ) ); + $this->assertSame( $colorized_string, Colors::decolorize( $colorized_string, 3 /*noop*/ ) ); + $this->assertSame( $both_string, Colors::decolorize( $both_string, 3 /*noop*/ ) ); + } + function test_strwidth() { // Save. $test_strwidth = getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); diff --git a/tests/test-table.php b/tests/test-table.php index 47a9670..34303fa 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -50,7 +50,6 @@ public function test_column_value_too_long_with_multibytes() { $table->addRow( array( 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', 'Hello' ) ); $out = $table->getDisplayLines(); - print_r( $out ); for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) + 1 ); } From 105bc9674ba0af27937fc9f5aa98056504db5f88 Mon Sep 17 00:00:00 2001 From: gitlost Date: Wed, 26 Jul 2017 14:38:04 +0100 Subject: [PATCH 09/29] Suppress tput stderr. Use COLUMNS env var 1st if available. --- lib/cli/Shell.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/cli/Shell.php b/lib/cli/Shell.php index 7b3f9bb..8962fdb 100755 --- a/lib/cli/Shell.php +++ b/lib/cli/Shell.php @@ -49,8 +49,10 @@ static public function columns() { } } } else { - if ( getenv( 'TERM' ) ) { - $columns = (int) exec( '/usr/bin/env tput cols' ); + if ( ! ( $columns = (int) getenv( 'COLUMNS' ) ) ) { + if ( getenv( 'TERM' ) ) { + $columns = (int) exec( '/usr/bin/env tput cols 2>/dev/null' ); + } } } } From 4cc65fdb478f2dd39ff49e8e963d82e4d7a11109 Mon Sep 17 00:00:00 2001 From: gitlost Date: Sat, 29 Jul 2017 03:15:15 +0100 Subject: [PATCH 10/29] Optimize double-width safe_substr when all double-width. --- lib/cli/cli.php | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/lib/cli/cli.php b/lib/cli/cli.php index 0c67346..7da86ac 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -203,15 +203,23 @@ function safe_substr( $str, $start, $length = false, $is_width = false, $encodin $eaw_regex = get_unicode_regexs( 'eaw' ); // If there's any East Asian double-width chars... if ( preg_match( $eaw_regex, $substr ) ) { - // Explode string into an array of UTF-8 chars. Based on core `_mb_substr()` in "wp-includes/compat.php". - $chars = preg_split( '/([\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*)/', $substr, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); - $cnt = min( count( $chars ), $length ); - $width = $length; + // Note that if the length ends in the middle of a double-width char, the char is included, not excluded. - for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { - $width -= preg_match( $eaw_regex, $chars[ $length ] ) ? 2 : 1; + // See if it's all EAW - the most likely case. + if ( preg_match_all( $eaw_regex, $substr ) === $length ) { + // Just halve the length so (rounded up). + $substr = mb_substr( $substr, 0, (int) ( ( $length + 1 ) / 2 ), $encoding ); + } else { + // Explode string into an array of UTF-8 chars. Based on core `_mb_substr()` in "wp-includes/compat.php". + $chars = preg_split( '/([\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*)/', $substr, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); + $cnt = min( count( $chars ), $length ); + $width = $length; + + for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { + $width -= preg_match( $eaw_regex, $chars[ $length ] ) ? 2 : 1; + } + return join( '', array_slice( $chars, 0, $length ) ); } - return join( '', array_slice( $chars, 0, $length ) ); } } } else { @@ -279,7 +287,7 @@ function strwidth( $string, $encoding = false ) { return $width; } } - return safe_strlen( $string ); + return safe_strlen( $string, $encoding ); } /** From e8ac443bba2c9178b069779b7077a101270524cd Mon Sep 17 00:00:00 2001 From: gitlost Date: Sat, 29 Jul 2017 03:22:07 +0100 Subject: [PATCH 11/29] Need dummy matches arg for preg_match_all() PHP 5.3 (arghh). --- lib/cli/cli.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/cli/cli.php b/lib/cli/cli.php index 7da86ac..b231c58 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -202,7 +202,7 @@ function safe_substr( $str, $start, $length = false, $is_width = false, $encodin // Set the East Asian Width regex. $eaw_regex = get_unicode_regexs( 'eaw' ); // If there's any East Asian double-width chars... - if ( preg_match( $eaw_regex, $substr ) ) { + if ( preg_match( $eaw_regex, $substr, $dummy /*needed for PHP 5.3*/ ) ) { // Note that if the length ends in the middle of a double-width char, the char is included, not excluded. // See if it's all EAW - the most likely case. From cacbdf43a0cab92f634c62ed86579f2c43ab65e4 Mon Sep 17 00:00:00 2001 From: gitlost Date: Sat, 29 Jul 2017 03:26:09 +0100 Subject: [PATCH 12/29] Er, put it in the right place (arghh). --- lib/cli/cli.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/cli/cli.php b/lib/cli/cli.php index b231c58..8bbe731 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -202,11 +202,11 @@ function safe_substr( $str, $start, $length = false, $is_width = false, $encodin // Set the East Asian Width regex. $eaw_regex = get_unicode_regexs( 'eaw' ); // If there's any East Asian double-width chars... - if ( preg_match( $eaw_regex, $substr, $dummy /*needed for PHP 5.3*/ ) ) { + if ( preg_match( $eaw_regex, $substr ) ) { // Note that if the length ends in the middle of a double-width char, the char is included, not excluded. // See if it's all EAW - the most likely case. - if ( preg_match_all( $eaw_regex, $substr ) === $length ) { + if ( preg_match_all( $eaw_regex, $substr, $dummy /*needed for PHP 5.3*/ ) === $length ) { // Just halve the length so (rounded up). $substr = mb_substr( $substr, 0, (int) ( ( $length + 1 ) / 2 ), $encoding ); } else { From 7667f5e367af9fadba8e8e314aa7048020ef363b Mon Sep 17 00:00:00 2001 From: gitlost Date: Sat, 29 Jul 2017 10:09:16 +0100 Subject: [PATCH 13/29] Round down instead of up in safe_strlen(). --- lib/cli/cli.php | 10 +++++++--- lib/cli/table/Ascii.php | 2 +- tests/test-cli.php | 10 +++++----- tests/test-table.php | 44 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 57 insertions(+), 9 deletions(-) diff --git a/lib/cli/cli.php b/lib/cli/cli.php index 8bbe731..f916bde 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -203,12 +203,12 @@ function safe_substr( $str, $start, $length = false, $is_width = false, $encodin $eaw_regex = get_unicode_regexs( 'eaw' ); // If there's any East Asian double-width chars... if ( preg_match( $eaw_regex, $substr ) ) { - // Note that if the length ends in the middle of a double-width char, the char is included, not excluded. + // Note that if the length ends in the middle of a double-width char, the char is excluded, not included. // See if it's all EAW - the most likely case. if ( preg_match_all( $eaw_regex, $substr, $dummy /*needed for PHP 5.3*/ ) === $length ) { - // Just halve the length so (rounded up). - $substr = mb_substr( $substr, 0, (int) ( ( $length + 1 ) / 2 ), $encoding ); + // Just halve the length so (rounded down to a minimum of 1). + $substr = mb_substr( $substr, 0, max( (int) ( $length / 2 ), 1 ), $encoding ); } else { // Explode string into an array of UTF-8 chars. Based on core `_mb_substr()` in "wp-includes/compat.php". $chars = preg_split( '/([\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*)/', $substr, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); @@ -218,6 +218,10 @@ function safe_substr( $str, $start, $length = false, $is_width = false, $encodin for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { $width -= preg_match( $eaw_regex, $chars[ $length ] ) ? 2 : 1; } + // Round down to a minimum of 1. + if ( $width < 0 && $length > 1 ) { + $length--; + } return join( '', array_slice( $chars, 0, $length ) ); } } diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index 3372a9b..e2b1c62 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -136,7 +136,7 @@ public function row( array $row ) { $col_width = $this->_widths[ $col ]; $encoding = function_exists( 'mb_detect_encoding' ) ? mb_detect_encoding( $value, null, true /*strict*/ ) : false; $original_val_width = Colors::width( $value, self::isPreColorized( $col ), $encoding ); - if ( $original_val_width > $col_width ) { + if ( $col_width && $original_val_width > $col_width ) { $row[ $col ] = \cli\safe_substr( $value, 0, $col_width, true /*is_width*/, $encoding ); $value = \cli\safe_substr( $value, \cli\safe_strlen( $row[ $col ], $encoding ), null /*length*/, false /*is_width*/, $encoding ); $i = 0; diff --git a/tests/test-cli.php b/tests/test-cli.php index 6ab4a0c..bc1fe02 100644 --- a/tests/test-cli.php +++ b/tests/test-cli.php @@ -95,8 +95,8 @@ function test_encoded_substr_is_width() { $this->assertSame( 'he', \cli\safe_substr( Colors::pad( 'hello', 6 ), 0, 2, true /*is_width*/ ) ); $this->assertSame( 'ór', \cli\safe_substr( Colors::pad( 'óra', 6 ), 0, 2, true /*is_width*/ ) ); $this->assertSame( '日', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 2, true /*is_width*/ ) ); + $this->assertSame( '日', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 3, true /*is_width*/ ) ); $this->assertSame( '日本', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 4, true /*is_width*/ ) ); - $this->assertSame( '日本', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 3, true /*is_width*/ ) ); $this->assertSame( '日本語', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 6, true /*is_width*/ ) ); $this->assertSame( '日本語 ', \cli\safe_substr( Colors::pad( '日本語', 8 ), 0, 7, true /*is_width*/ ) ); @@ -107,12 +107,12 @@ function test_encoded_substr_is_width() { $this->assertSame( '', \cli\safe_substr( '1日4本語90', 0, 0, true /*is_width*/ ) ); $this->assertSame( '1', \cli\safe_substr( '1日4本語90', 0, 1, true /*is_width*/ ) ); - $this->assertSame( '1日', \cli\safe_substr( '1日4本語90', 0, 2, true /*is_width*/ ) ); + $this->assertSame( '1', \cli\safe_substr( '1日4本語90', 0, 2, true /*is_width*/ ) ); $this->assertSame( '1日', \cli\safe_substr( '1日4本語90', 0, 3, true /*is_width*/ ) ); $this->assertSame( '1日4', \cli\safe_substr( '1日4本語90', 0, 4, true /*is_width*/ ) ); - $this->assertSame( '1日4本', \cli\safe_substr( '1日4本語90', 0, 5, true /*is_width*/ ) ); + $this->assertSame( '1日4', \cli\safe_substr( '1日4本語90', 0, 5, true /*is_width*/ ) ); $this->assertSame( '1日4本', \cli\safe_substr( '1日4本語90', 0, 6, true /*is_width*/ ) ); - $this->assertSame( '1日4本語', \cli\safe_substr( '1日4本語90', 0, 7, true /*is_width*/ ) ); + $this->assertSame( '1日4本', \cli\safe_substr( '1日4本語90', 0, 7, true /*is_width*/ ) ); $this->assertSame( '1日4本語', \cli\safe_substr( '1日4本語90', 0, 8, true /*is_width*/ ) ); $this->assertSame( '1日4本語9', \cli\safe_substr( '1日4本語90', 0, 9, true /*is_width*/ ) ); $this->assertSame( '1日4本語90', \cli\safe_substr( '1日4本語90', 0, 10, true /*is_width*/ ) ); @@ -124,7 +124,7 @@ function test_encoded_substr_is_width() { $this->assertSame( '本', \cli\safe_substr( '1日4本語90', 3, 1, true /*is_width*/ ) ); $this->assertSame( '本', \cli\safe_substr( '1日4本語90', 3, 2, true /*is_width*/ ) ); - $this->assertSame( '本語', \cli\safe_substr( '1日4本語90', 3, 3, true /*is_width*/ ) ); + $this->assertSame( '本', \cli\safe_substr( '1日4本語90', 3, 3, true /*is_width*/ ) ); $this->assertSame( '本語', \cli\safe_substr( '1日4本語90', 3, 4, true /*is_width*/ ) ); $this->assertSame( '本語9', \cli\safe_substr( '1日4本語90', 3, 5, true /*is_width*/ ) ); diff --git a/tests/test-table.php b/tests/test-table.php index 34303fa..29e192b 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -55,6 +55,50 @@ public function test_column_value_too_long_with_multibytes() { } } + public function test_column_odd_single_width_with_double_width() { + + $dummy = new cli\Table; + $renderer = new cli\Table\Ascii; + + $strip_borders = function ( $a ) { + return array_map( function ( $v ) { + return substr( $v, 2, -2 ); + }, $a ); + }; + + $renderer->setWidths( array( 10 ) ); + + // 1 single-width, 6 double-width, 1 single-width, 2 double-width, 1 half-width, 2 double-width. + $out = $renderer->row( array( '1あいうえおか2きくカけこ' ) ); + $result = $strip_borders( explode( "\n", $out ) ); + + $this->assertSame( 3, count( $result ) ); + $this->assertSame( '1あいうえ ', $result[0] ); // 1 single width, 4 double-width, space = 10. + $this->assertSame( 'おか2きくカ', $result[1] ); // 2 double-width, 1 single-width, 2 double-width, 1 half-width = 10. + $this->assertSame( 'けこ ', $result[2] ); // 2 double-width, 8 spaces = 10. + + // Minimum width 1. + + $renderer->setWidths( array( 1 ) ); + + $out = $renderer->row( array( '1あいうえおか2きくカけこ' ) ); + $result = $strip_borders( explode( "\n", $out ) ); + + $this->assertSame( 13, count( $result ) ); + // Uneven rows. + $this->assertSame( '1', $result[0] ); + $this->assertSame( 'あ', $result[1] ); + + // Zero width does no wrapping. + + $renderer->setWidths( array( 0 ) ); + + $out = $renderer->row( array( '1あいうえおか2きくカけこ' ) ); + $result = $strip_borders( explode( "\n", $out ) ); + + $this->assertSame( 1, count( $result ) ); + } + public function test_ascii_pre_colorized_widths() { Colors::enable( true ); From 168e5bc957460cc17b7cd99594adae8f0e920fe4 Mon Sep 17 00:00:00 2001 From: miya0001 Date: Sat, 29 Jul 2017 19:25:49 +0900 Subject: [PATCH 14/29] add single-witdh char to test --- tests/test-table.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test-table.php b/tests/test-table.php index 29e192b..65b782f 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -46,10 +46,11 @@ public function test_column_value_too_long_with_multibytes() { $renderer->setConstraintWidth( $constraint_width ); $table->setRenderer( $renderer ); $table->setHeaders( array( 'Field', 'Value' ) ); - $table->addRow( array( 'この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。この文章はダミーです。文字の大きさ、', 'こんにちは' ) ); + $table->addRow( array( '1この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。', 'こんにちは' ) ); $table->addRow( array( 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', 'Hello' ) ); $out = $table->getDisplayLines(); + print_r($out); for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) + 1 ); } From 0685ba2de52728ad7d51b9e01897541dad592e48 Mon Sep 17 00:00:00 2001 From: miya0001 Date: Sat, 29 Jul 2017 19:27:40 +0900 Subject: [PATCH 15/29] remove print_r(). --- tests/test-table.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test-table.php b/tests/test-table.php index 65b782f..d8b5a97 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -50,7 +50,6 @@ public function test_column_value_too_long_with_multibytes() { $table->addRow( array( 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', 'Hello' ) ); $out = $table->getDisplayLines(); - print_r($out); for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) + 1 ); } From 18b8e3d71fc58b7877a3993c3cda5b2b2ab85d74 Mon Sep 17 00:00:00 2001 From: miya0001 Date: Sat, 29 Jul 2017 19:29:04 +0900 Subject: [PATCH 16/29] add single-width char to second line --- tests/test-table.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test-table.php b/tests/test-table.php index d8b5a97..9a8153a 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -46,7 +46,7 @@ public function test_column_value_too_long_with_multibytes() { $renderer->setConstraintWidth( $constraint_width ); $table->setRenderer( $renderer ); $table->setHeaders( array( 'Field', 'Value' ) ); - $table->addRow( array( '1この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。', 'こんにちは' ) ); + $table->addRow( array( '1この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。2この文章はダミーです。文字の大きさ、量、字間、行間等を確認するために入れています。', 'こんにちは' ) ); $table->addRow( array( 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.', 'Hello' ) ); $out = $table->getDisplayLines(); From 65b5ac2612ac6f6741634a9d54f34a7ba9975fbb Mon Sep 17 00:00:00 2001 From: gitlost Date: Sun, 30 Jul 2017 23:14:43 +0100 Subject: [PATCH 17/29] Fix reverse (inverse) and white (grey). --- lib/cli/Colors.php | 8 ++++---- tests/test-colors.php | 27 +++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 tests/test-colors.php diff --git a/lib/cli/Colors.php b/lib/cli/Colors.php index e764ec8..6b0a64d 100644 --- a/lib/cli/Colors.php +++ b/lib/cli/Colors.php @@ -231,7 +231,7 @@ static public function getColors() { '%p' => array('color' => 'magenta'), '%m' => array('color' => 'magenta'), '%c' => array('color' => 'cyan'), - '%w' => array('color' => 'grey'), + '%w' => array('color' => 'white'), '%k' => array('color' => 'black'), '%n' => array('color' => 'reset'), '%Y' => array('color' => 'yellow', 'style' => 'bright'), @@ -241,7 +241,7 @@ static public function getColors() { '%P' => array('color' => 'magenta', 'style' => 'bright'), '%M' => array('color' => 'magenta', 'style' => 'bright'), '%C' => array('color' => 'cyan', 'style' => 'bright'), - '%W' => array('color' => 'grey', 'style' => 'bright'), + '%W' => array('color' => 'white', 'style' => 'bright'), '%K' => array('color' => 'black', 'style' => 'bright'), '%N' => array('color' => 'reset', 'style' => 'bright'), '%3' => array('background' => 'yellow'), @@ -250,11 +250,11 @@ static public function getColors() { '%1' => array('background' => 'red'), '%5' => array('background' => 'magenta'), '%6' => array('background' => 'cyan'), - '%7' => array('background' => 'grey'), + '%7' => array('background' => 'white'), '%0' => array('background' => 'black'), '%F' => array('style' => 'blink'), '%U' => array('style' => 'underline'), - '%8' => array('style' => 'inverse'), + '%8' => array('style' => 'reverse'), '%9' => array('style' => 'bright'), '%_' => array('style' => 'bright') ); diff --git a/tests/test-colors.php b/tests/test-colors.php new file mode 100644 index 0000000..649d0ec --- /dev/null +++ b/tests/test-colors.php @@ -0,0 +1,27 @@ +assertSame( Colors::colorize( $str ), Colors::color( $color ) ); + if ( in_array( 'reset', $color ) ) { + $this->assertTrue( false !== strpos( $colored, '[0m' ) ); + } else { + $this->assertTrue( false === strpos( $colored, '[0m' ) ); + } + } + + function dataColors() { + $ret = array(); + foreach ( Colors::getColors() as $str => $color ) { + $ret[] = array( $str, $color ); + } + return $ret; + } +} From f8bb561e30caebdd74a1294c3d0e865f65c25e48 Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 11:17:52 +0100 Subject: [PATCH 18/29] Use grapheme_substr & pcre_match in safe_substr. Ascii::columns fix. --- lib/cli/Shell.php | 10 ++- lib/cli/cli.php | 188 ++++++++++++++++++++++++++++------------ lib/cli/table/Ascii.php | 10 +-- tests/test-cli.php | 163 +++++++++++++++++++++++++++++++++- tests/test-colors.php | 3 + tests/test-shell.php | 1 + tests/test-table.php | 105 ++++++++++++++++++---- 7 files changed, 401 insertions(+), 79 deletions(-) diff --git a/lib/cli/Shell.php b/lib/cli/Shell.php index 8962fdb..9479c6b 100755 --- a/lib/cli/Shell.php +++ b/lib/cli/Shell.php @@ -50,8 +50,14 @@ static public function columns() { } } else { if ( ! ( $columns = (int) getenv( 'COLUMNS' ) ) ) { - if ( getenv( 'TERM' ) ) { - $columns = (int) exec( '/usr/bin/env tput cols 2>/dev/null' ); + $size = exec( '/usr/bin/env stty size 2>/dev/null' ); + if ( '' !== $size && preg_match( '/[0-9]+ ([0-9]+)/', $size, $matches ) ) { + $columns = (int) $matches[1]; + } + if ( ! $columns ) { + if ( getenv( 'TERM' ) ) { + $columns = (int) exec( '/usr/bin/env tput cols 2>/dev/null' ); + } } } } diff --git a/lib/cli/cli.php b/lib/cli/cli.php index f916bde..a55f61e 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -153,87 +153,148 @@ function menu( $items, $default = null, $title = 'Choose an item' ) { } /** - * Attempts an encoding-safe way of getting string length. If mb_string extensions aren't - * installed, falls back to basic strlen if no encoding is present + * Attempts an encoding-safe way of getting string length. If intl extension or PCRE with '\X' or mb_string extension aren't + * available, falls back to basic strlen. * * @param string $str The string to check. * @param string|bool $encoding Optional. The encoding of the string. Default false. * @return int Numeric value that represents the string's length */ function safe_strlen( $str, $encoding = false ) { + // Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strlen(), "other" strlen(). + $test_safe_strlen = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); + + // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string. + if ( ( ! $encoding || 'UTF-8' === $encoding ) && function_exists( 'grapheme_strlen' ) && null !== ( $length = grapheme_strlen( $str ) ) ) { + if ( ! $test_safe_strlen || ( $test_safe_strlen & 1 ) ) { + return $length; + } + } + // Assume UTF-8 if no encoding given - `preg_match_all()` will return false if given non-UTF-8 string. + if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() && false !== ( $length = preg_match_all( '/\X/u', $str, $dummy /*needed for PHP 5.3*/ ) ) ) { + if ( ! $test_safe_strlen || ( $test_safe_strlen & 2 ) ) { + return $length; + } + } + // Legacy encodings and old PHPs will reach here. if ( function_exists( 'mb_strlen' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { if ( ! $encoding ) { $encoding = mb_detect_encoding( $str, null, true /*strict*/ ); } - $length = mb_strlen( $str, $encoding ); - } else { - // iconv will return PHP notice if non-ascii characters are present in input string - $str = iconv( $encoding ? $encoding : 'ASCII', 'ASCII', $str ); - - $length = strlen( $str ); + $length = mb_strlen( $str, $encoding ); + if ( 'UTF-8' === $encoding ) { + // Subtract combining characters. + $length -= preg_match_all( get_unicode_regexs( 'm' ), $str, $dummy /*needed for PHP 5.3*/ ); + } + if ( ! $test_safe_strlen || ( $test_safe_strlen & 4 ) ) { + return $length; + } } - - return $length; + return strlen( $str ); } /** - * Attempts an encoding-safe way of getting a substring. If mb_string extensions aren't - * installed, falls back to ascii substring if no encoding is present + * Attempts an encoding-safe way of getting a substring. If intl extension or PCRE with '\X' or mb_string extension aren't + * available, falls back to substr(). * * @param string $str The input string. * @param int $start The starting position of the substring. - * @param int|bool|null $length Optional. Maximum length of the substring. Default false. - * @param int|bool $is_width Optional. If set and encoding is UTF-8, $length is interpreted as spacing width. Default false. + * @param int|bool|null $length Optional, unless $is_width is set. Maximum length of the substring. Default false. Negative not supported. + * @param int|bool $is_width Optional. If set and encoding is UTF-8, $length (which must be specified) is interpreted as spacing width. Default false. * @param string|bool $encoding Optional. The encoding of the string. Default false. - * @return string Substring of string specified by start and length parameters + * @return bool|string False if given unsupported args, otherwise substring of string specified by start and length parameters */ function safe_substr( $str, $start, $length = false, $is_width = false, $encoding = false ) { + // Negative $length or $is_width and $length not specified not supported. + if ( $length < 0 || ( $is_width && ( null === $length || false === $length ) ) ) { + return false; + } + $have_safe_strlen = false; // PHP 5.3 substr takes false as full length, PHP > 5.3 takes null - for compat. do `safe_strlen()`. if ( null === $length || false === $length ) { $length = safe_strlen( $str, $encoding ); + $have_safe_strlen = true; } + + // Allow for selective testings - "1" bit set tests grapheme_substr(), "2" preg_match( '/\X/' ), "4" mb_substr(), "8" substr(). + $test_safe_substr = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); + + // Assume UTF-8 if no encoding given - `grapheme_substr()` will return false (not null like `grapheme_strlen()`) if given non-UTF-8 string. + if ( ( ! $encoding || 'UTF-8' === $encoding ) && function_exists( 'grapheme_substr' ) && false !== ( $try = grapheme_substr( $str, $start, $length ) ) ) { + if ( ! $test_safe_substr || ( $test_safe_substr & 1 ) ) { + return $is_width ? _safe_substr_eaw( $try, $length ) : $try; + } + } + // Assume UTF-8 if no encoding given - `preg_match()` will return false if given non-UTF-8 string. + if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() ) { + if ( $start < 0 ) { + $start = max( $start + ( $have_safe_strlen ? $length : safe_strlen( $str, $encoding ) ), 0 ); + } + if ( $start ) { + if ( preg_match( '/^\X{' . $start . '}(\X{0,' . $length . '})/u', $str, $matches ) ) { + if ( ! $test_safe_substr || ( $test_safe_substr & 2 ) ) { + return $is_width ? _safe_substr_eaw( $matches[1], $length ) : $matches[1]; + } + } + } else { + if ( preg_match( '/^\X{0,' . $length . '}/u', $str, $matches ) ) { + if ( ! $test_safe_substr || ( $test_safe_substr & 2 ) ) { + return $is_width ? _safe_substr_eaw( $matches[0], $length ) : $matches[0]; + } + } + } + } + // Legacy encodings and old PHPs will reach here. if ( function_exists( 'mb_substr' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { if ( ! $encoding ) { $encoding = mb_detect_encoding( $str, null, true /*strict*/ ); } - $substr = mb_substr( $str, $start, $length, $encoding ); - - if ( $is_width && 'UTF-8' === $encoding ) { - // Set the East Asian Width regex. - $eaw_regex = get_unicode_regexs( 'eaw' ); - // If there's any East Asian double-width chars... - if ( preg_match( $eaw_regex, $substr ) ) { - // Note that if the length ends in the middle of a double-width char, the char is excluded, not included. - - // See if it's all EAW - the most likely case. - if ( preg_match_all( $eaw_regex, $substr, $dummy /*needed for PHP 5.3*/ ) === $length ) { - // Just halve the length so (rounded down to a minimum of 1). - $substr = mb_substr( $substr, 0, max( (int) ( $length / 2 ), 1 ), $encoding ); - } else { - // Explode string into an array of UTF-8 chars. Based on core `_mb_substr()` in "wp-includes/compat.php". - $chars = preg_split( '/([\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*)/', $substr, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); - $cnt = min( count( $chars ), $length ); - $width = $length; - - for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { - $width -= preg_match( $eaw_regex, $chars[ $length ] ) ? 2 : 1; - } - // Round down to a minimum of 1. - if ( $width < 0 && $length > 1 ) { - $length--; - } - return join( '', array_slice( $chars, 0, $length ) ); - } - } + // Bug: not adjusting for combining chars. + $try = mb_substr( $str, $start, $length, $encoding ); + if ( 'UTF-8' === $encoding && $is_width ) { + $try = _safe_substr_eaw( $try, $length ); + } + if ( ! $test_safe_substr || ( $test_safe_substr & 4 ) ) { + return $try; } - } else { - // iconv will return PHP notice if non-ascii characters are present in input string - $str = iconv( $encoding ? $encoding : 'ASCII', 'ASCII', $str ); - - $substr = substr( $str, $start, $length ); } + return substr( $str, $start, $length ); +} + +/** + * Internal function used by `safe_substr()` to adjust for East Asian double-width chars. + * + * @return string + */ +function _safe_substr_eaw( $str, $length ) { + // Set the East Asian Width regex. + $eaw_regex = get_unicode_regexs( 'eaw' ); + + // If there's any East Asian double-width chars... + if ( preg_match( $eaw_regex, $str ) ) { + // Note that if the length ends in the middle of a double-width char, the char is excluded, not included. - return $substr; + // See if it's all EAW. + if ( preg_match_all( $eaw_regex, $str, $dummy /*needed for PHP 5.3*/ ) === $length ) { + // Just halve the length so (rounded down to a minimum of 1). + $str = mb_substr( $str, 0, max( (int) ( $length / 2 ), 1 ), 'UTF-8' ); + } else { + // Explode string into an array of UTF-8 chars. Based on core `_mb_substr()` in "wp-includes/compat.php". + $chars = preg_split( '/([\x00-\x7f\xc2-\xf4][^\x00-\x7f\xc2-\xf4]*)/', $str, $length + 1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY ); + $cnt = min( count( $chars ), $length ); + $width = $length; + + for ( $length = 0; $length < $cnt && $width > 0; $length++ ) { + $width -= preg_match( $eaw_regex, $chars[ $length ] ) ? 2 : 1; + } + // Round down to a minimum of 1. + if ( $width < 0 && $length > 1 ) { + $length--; + } + return join( '', array_slice( $chars, 0, $length ) ); + } + } + return $str; } /** @@ -266,18 +327,19 @@ function strwidth( $string, $encoding = false ) { // Allow for selective testings - "1" bit set tests grapheme_strlen(), "2" preg_match_all( '/\X/u' ), "4" mb_strwidth(), "other" safe_strlen(). $test_strwidth = getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); - // Assume UTF-8 - `grapheme_strlen()` will return null if given non-UTF-8 string. - if ( function_exists( 'grapheme_strlen' ) && null !== ( $width = grapheme_strlen( $string ) ) ) { + // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string. + if ( ( ! $encoding || 'UTF-8' === $encoding ) && function_exists( 'grapheme_strlen' ) && null !== ( $width = grapheme_strlen( $string ) ) ) { if ( ! $test_strwidth || ( $test_strwidth & 1 ) ) { return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ ); } } - // Assume UTF-8 - `preg_match_all()` will return false if given non-UTF-8 string (or if PCRE UTF-8 mode is unavailable). - if ( false !== ( $width = preg_match_all( '/\X/u', $string, $dummy /*needed for PHP 5.3*/ ) ) ) { + // Assume UTF-8 if no encoding given - `preg_match_all()` will return false if given non-UTF-8 string. + if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_pcre_x() && false !== ( $width = preg_match_all( '/\X/u', $string, $dummy /*needed for PHP 5.3*/ ) ) ) { if ( ! $test_strwidth || ( $test_strwidth & 2 ) ) { return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ ); } } + // Legacy encodings and old PHPs will reach here. if ( function_exists( 'mb_strwidth' ) && ( $encoding || function_exists( 'mb_detect_encoding' ) ) ) { if ( ! $encoding ) { $encoding = mb_detect_encoding( $string, null, true /*strict*/ ); @@ -294,6 +356,24 @@ function strwidth( $string, $encoding = false ) { return safe_strlen( $string, $encoding ); } +/** + * Returns whether PCRE Unicode extended grapheme cluster '\X' is available for use. + * + * @return bool + */ +function can_use_pcre_x() { + static $can_use_pcre_x = null; + + if ( null === $can_use_pcre_x ) { + // '\X' introduced (as Unicde extended grapheme cluster) in PCRE 8.32 - see https://vcs.pcre.org/pcre/code/tags/pcre-8.32/ChangeLog?view=markup line 53. + // Older versions of PCRE were bundled with PHP <= 5.3.23 & <= 5.4.13. + $unfc_pcre_version = substr( PCRE_VERSION, 0, strspn( PCRE_VERSION, '0123456789.' ) ); // Remove any trailing date stuff. + $can_use_pcre_x = version_compare( $unfc_pcre_version, '8.32', '>=' ) && false !== @preg_match( '/\X/u', '' ); + } + + return $can_use_pcre_x; +} + /** * Get the regexs generated from Unicode data. * diff --git a/lib/cli/table/Ascii.php b/lib/cli/table/Ascii.php index e2b1c62..d59729b 100644 --- a/lib/cli/table/Ascii.php +++ b/lib/cli/table/Ascii.php @@ -40,8 +40,8 @@ public function setWidths(array $widths) { $this->_constraintWidth = (int) Shell::columns(); } $col_count = count( $widths ); - $col_borders_count = $col_count * strlen( $this->_characters['border'] ); - $table_borders_count = strlen( $this->_characters['border'] ) * 1; + $col_borders_count = $col_count ? ( ( $col_count - 1 ) * strlen( $this->_characters['border'] ) ) : 0; + $table_borders_count = strlen( $this->_characters['border'] ) * 2; $col_padding_count = $col_count * strlen( $this->_characters['padding'] ) * 2; $max_width = $this->_constraintWidth - $col_borders_count - $table_borders_count - $col_padding_count; @@ -63,11 +63,11 @@ public function setWidths(array $widths) { foreach( $widths as &$width ) { if ( in_array( $width, $resize_widths ) ) { $width = $avg + $avg_extra_width; - $extra_width = $extra_width - $avg_extra_width; array_shift( $resize_widths ); // Last item gets the cake if ( empty( $resize_widths ) ) { - $width = $width + $extra_width; + $width = 0; // Zero it so not in sum. + $width = $max_width - array_sum( $widths ); } } } @@ -131,7 +131,7 @@ public function row( array $row ) { foreach( $row as $col => $value ) { - $value = str_replace( PHP_EOL, ' ', $value ); + $value = str_replace( array( "\r\n", "\n" ), ' ', $value ); $col_width = $this->_widths[ $col ]; $encoding = function_exists( 'mb_detect_encoding' ) ? mb_detect_encoding( $value, null, true /*strict*/ ) : false; diff --git a/tests/test-cli.php b/tests/test-cli.php index bc1fe02..b404803 100644 --- a/tests/test-cli.php +++ b/tests/test-cli.php @@ -87,10 +87,105 @@ function test_encoded_substr() { $this->assertSame( '本語', \cli\safe_substr( Colors::pad( '日本語', 8 ), 1, 2 ) ); $this->assertSame( '語 ', \cli\safe_substr( Colors::pad( '日本語', 8 ), 2, 2 ) ); + $this->assertSame( ' ', \cli\safe_substr( Colors::pad( '日本語', 8 ), -1 ) ); + $this->assertSame( ' ', \cli\safe_substr( Colors::pad( '日本語', 8 ), -1, 2 ) ); + $this->assertSame( '語 ', \cli\safe_substr( Colors::pad( '日本語', 8 ), -3, 3 ) ); + } + + function test_various_substr() { + // Save. + $test_safe_substr = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); + if ( function_exists( 'mb_detect_order' ) ) { + $mb_detect_order = mb_detect_order(); + } + + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); + + // Latin, kana, Latin, Latin combining, Thai combining, Hangul. + $str = 'lムnöม้p를'; + + if ( function_exists( 'grapheme_substr' ) ) { + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=1' ); // Tests grapheme_substr(). + $this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) ); + $this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) ); + $this->assertSame( 'lム', \cli\safe_substr( $str, 0, 2 ) ); + $this->assertSame( 'lムn', \cli\safe_substr( $str, 0, 3 ) ); + $this->assertSame( 'lムnö', \cli\safe_substr( $str, 0, 4 ) ); + $this->assertSame( 'lムnöม้', \cli\safe_substr( $str, 0, 5 ) ); + $this->assertSame( 'lムnöม้p', \cli\safe_substr( $str, 0, 6 ) ); + $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 7 ) ); + $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 8 ) ); + $this->assertSame( '를', \cli\safe_substr( $str, -1 ) ); + $this->assertSame( 'p를', \cli\safe_substr( $str, -2 ) ); + $this->assertSame( 'ม้p를', \cli\safe_substr( $str, -3 ) ); + $this->assertSame( 'öม้p를', \cli\safe_substr( $str, -4 ) ); + $this->assertSame( 'öม้p', \cli\safe_substr( $str, -4, 3 ) ); + $this->assertSame( 'nö', \cli\safe_substr( $str, -5, 2 ) ); + $this->assertSame( 'ム', \cli\safe_substr( $str, -6, 1 ) ); + $this->assertSame( 'ムnöม้p를', \cli\safe_substr( $str, -6 ) ); + $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -7 ) ); + $this->assertSame( 'lムnö', \cli\safe_substr( $str, -7, 4 ) ); + // $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -8 ) ); // grapheme_substr() returns false on this. + } + + if ( \cli\can_use_pcre_x() ) { + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=2' ); // Tests preg_match( '/\X/u' ). + $this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) ); + $this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) ); + $this->assertSame( 'lム', \cli\safe_substr( $str, 0, 2 ) ); + $this->assertSame( 'lムn', \cli\safe_substr( $str, 0, 3 ) ); + $this->assertSame( 'lムnö', \cli\safe_substr( $str, 0, 4 ) ); + $this->assertSame( 'lムnöม้', \cli\safe_substr( $str, 0, 5 ) ); + $this->assertSame( 'lムnöม้p', \cli\safe_substr( $str, 0, 6 ) ); + $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 7 ) ); + $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, 0, 8 ) ); + $this->assertSame( '를', \cli\safe_substr( $str, -1 ) ); + $this->assertSame( 'p를', \cli\safe_substr( $str, -2 ) ); + $this->assertSame( 'ม้p를', \cli\safe_substr( $str, -3 ) ); + $this->assertSame( 'öม้p를', \cli\safe_substr( $str, -4 ) ); + $this->assertSame( 'öม้p', \cli\safe_substr( $str, -4, 3 ) ); + $this->assertSame( 'nö', \cli\safe_substr( $str, -5, 2 ) ); + $this->assertSame( 'ム', \cli\safe_substr( $str, -6, 1 ) ); + $this->assertSame( 'ムnöม้p를', \cli\safe_substr( $str, -6 ) ); + $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -7 ) ); + $this->assertSame( 'lムnö', \cli\safe_substr( $str, -7, 4 ) ); + $this->assertSame( 'lムnöม้p를', \cli\safe_substr( $str, -8 ) ); + } + + if ( function_exists( 'mb_substr' ) ) { + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=4' ); // Tests mb_substr(). + $this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) ); + $this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) ); + $this->assertSame( 'lム', \cli\safe_substr( $str, 0, 2 ) ); + $this->assertSame( 'lムn', \cli\safe_substr( $str, 0, 3 ) ); + $this->assertSame( 'lムno', \cli\safe_substr( $str, 0, 4 ) ); // Wrong. + } + + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=8' ); // Tests substr(). + $this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) ); + $this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) ); + $this->assertSame( "l\xe3", \cli\safe_substr( $str, 0, 2 ) ); // Corrupt. + + // Non-UTF-8 - both grapheme_substr() and preg_match( '/\X/u' ) will fail. + + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); + if ( function_exists( 'mb_substr' ) && function_exists( 'mb_detect_order' ) ) { + // Latin-1 + mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) ); + $str = "\xe0b\xe7"; // "àbç" in ISO-8859-1 + $this->assertSame( "\xe0b", \cli\safe_substr( $str, 0, 2 ) ); + $this->assertSame( "\xe0b", mb_substr( $str, 0, 2, 'ISO-8859-1' ) ); + } + + // Restore. + putenv( false == $test_safe_substr ? 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' : "PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=$test_safe_substr" ); + if ( function_exists( 'mb_detect_order' ) ) { + mb_detect_order( $mb_detect_order ); + } } - function test_encoded_substr_is_width() { + function test_is_width_encoded_substr() { $this->assertSame( 'he', \cli\safe_substr( Colors::pad( 'hello', 6 ), 0, 2, true /*is_width*/ ) ); $this->assertSame( 'ór', \cli\safe_substr( Colors::pad( 'óra', 6 ), 0, 2, true /*is_width*/ ) ); @@ -131,6 +226,11 @@ function test_encoded_substr_is_width() { $this->assertSame( '0', \cli\safe_substr( '1日4本語90', 6, 1, true /*is_width*/ ) ); $this->assertSame( '', \cli\safe_substr( '1日4本語90', 7, 1, true /*is_width*/ ) ); $this->assertSame( '', \cli\safe_substr( '1日4本語90', 6, 0, true /*is_width*/ ) ); + + $this->assertSame( '0', \cli\safe_substr( '1日4本語90', -1, 3, true /*is_width*/ ) ); + $this->assertSame( '90', \cli\safe_substr( '1日4本語90', -2, 3, true /*is_width*/ ) ); + $this->assertSame( '語9', \cli\safe_substr( '1日4本語90', -3, 3, true /*is_width*/ ) ); + $this->assertSame( '本語9', \cli\safe_substr( '1日4本語90', -4, 5, true /*is_width*/ ) ); } function test_colorized_string_length() { @@ -296,8 +396,10 @@ function test_strwidth() { } putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=8' ); // Test safe_strlen(). - if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) { - $this->assertSame( 6, \cli\strwidth( $str ) ); // mb_strlen() - counts the 2 combining chars but not the double-width Han so out by 1. + if ( function_exists( 'grapheme_strlen' ) || \cli\can_use_pcre_x() ) { + $this->assertSame( 4, \cli\strwidth( $str ) ); // safe_strlen() (correctly) does not account for double-width Han so out by 1. + } elseif ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) { + $this->assertSame( 4, \cli\strwidth( $str ) ); // safe_strlen() (correctly) does not account for double-width Han so out by 1. $this->assertSame( 6, mb_strlen( $str, 'UTF-8' ) ); } else { $this->assertSame( 16, \cli\strwidth( $str ) ); // strlen() - no. of bytes. @@ -356,4 +458,59 @@ function test_strwidth() { mb_detect_order( $mb_detect_order ); } } + + function test_safe_strlen() { + // Save. + $test_safe_strlen = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); + if ( function_exists( 'mb_detect_order' ) ) { + $mb_detect_order = mb_detect_order(); + } + + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); + + // UTF-8. + + // ASCII l, 3-byte kana, ASCII n, ASCII o + 2-byte combining umlaut, 6-byte Thai combining, ASCII, 3-byte Hangul. grapheme length 7, bytes 18. + $str = 'lムnöม้p를'; + + if ( function_exists( 'grapheme_strlen' ) ) { + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); // Test grapheme_strlen(). + $this->assertSame( 7, \cli\safe_strlen( $str ) ); + if ( \cli\can_use_pcre_x() ) { + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN=2' ); // Test preg_match_all( '/\X/u' ). + $this->assertSame( 7, \cli\safe_strlen( $str ) ); + } + } elseif ( \cli\can_use_pcre_x() ) { + $this->assertSame( 7, \cli\safe_strlen( $str ) ); // Tests preg_match_all( '/\X/u' ). + } else { + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN=8' ); // Test strlen(). + $this->assertSame( 18, \cli\safe_strlen( $str ) ); // strlen() - no. of bytes. + $this->assertSame( 18, strlen( $str ) ); + } + + if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) { + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN=4' ); // Test mb_strlen(). + mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) ); + $this->assertSame( 7, \cli\safe_strlen( $str ) ); + $this->assertSame( 9, mb_strlen( $str, 'UTF-8' ) ); // mb_strlen() - counts the 2 combining chars. + } + + // Non-UTF-8 - both grapheme_strlen() and preg_match_all( '/\X/u' ) will fail. + + putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); + + if ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) { + // Latin-1 + mb_detect_order( array( 'UTF-8', 'ISO-8859-1' ) ); + $str = "\xe0b\xe7"; // "àbç" in ISO-8859-1 + $this->assertSame( 3, \cli\safe_strlen( $str ) ); // Test mb_strlen(). + $this->assertSame( 3, mb_strlen( $str, 'ISO-8859-1' ) ); + } + + // Restore. + putenv( false == $test_safe_strlen ? 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' : "PHP_CLI_TOOLS_TEST_SAFE_STRLEN=$test_safe_strlen" ); + if ( function_exists( 'mb_detect_order' ) ) { + mb_detect_order( $mb_detect_order ); + } + } } diff --git a/tests/test-colors.php b/tests/test-colors.php index 649d0ec..e7be7a0 100644 --- a/tests/test-colors.php +++ b/tests/test-colors.php @@ -8,6 +8,9 @@ class testsColors extends PHPUnit_Framework_TestCase { * @dataProvider dataColors */ function testColors( $str, $color ) { + // Colors enabled. + Colors::enable( true ); + $colored = Colors::color( $color ); $this->assertSame( Colors::colorize( $str ), Colors::color( $color ) ); if ( in_array( 'reset', $color ) ) { diff --git a/tests/test-shell.php b/tests/test-shell.php index 702948c..3db9c02 100644 --- a/tests/test-shell.php +++ b/tests/test-shell.php @@ -22,6 +22,7 @@ function testColumns() { // No TERM should result in default 80. putenv( 'TERM' ); + putenv( 'COLUMNS=80' ); putenv( 'WP_CLI_TEST_IS_WINDOWS=0' ); $columns = cli\Shell::columns(); diff --git a/tests/test-table.php b/tests/test-table.php index 9a8153a..685a531 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -7,7 +7,7 @@ */ class Test_Table extends PHPUnit_Framework_TestCase { - public function test_column_value_too_long() { + public function test_column_value_too_long_ascii() { $constraint_width = 80; @@ -20,21 +20,30 @@ public function test_column_value_too_long() { $table->addRow( array( 'author', 'the WordPress team' ) ); $out = $table->getDisplayLines(); - // "+ 1" accommodates "\n" $this->assertCount( 12, $out ); - $this->assertEquals( $constraint_width, strlen( $out[0] ) + 1 ); - $this->assertEquals( $constraint_width, strlen( $out[1] ) + 1 ); - $this->assertEquals( $constraint_width, strlen( $out[2] ) + 1 ); - $this->assertEquals( $constraint_width, strlen( $out[3] ) + 1 ); - $this->assertEquals( $constraint_width, strlen( $out[4] ) + 1 ); - $this->assertEquals( $constraint_width, strlen( $out[5] ) + 1 ); - $this->assertEquals( $constraint_width, strlen( $out[6] ) + 1 ); - $this->assertEquals( $constraint_width, strlen( $out[7] ) + 1 ); - $this->assertEquals( $constraint_width, strlen( $out[8] ) + 1 ); - $this->assertEquals( $constraint_width, strlen( $out[9] ) + 1 ); - $this->assertEquals( $constraint_width, strlen( $out[10] ) + 1 ); - $this->assertEquals( $constraint_width, strlen( $out[11] ) + 1 ); + $this->assertEquals( $constraint_width, strlen( $out[0] ) ); + $this->assertEquals( $constraint_width, strlen( $out[1] ) ); + $this->assertEquals( $constraint_width, strlen( $out[2] ) ); + $this->assertEquals( $constraint_width, strlen( $out[3] ) ); + $this->assertEquals( $constraint_width, strlen( $out[4] ) ); + $this->assertEquals( $constraint_width, strlen( $out[5] ) ); + $this->assertEquals( $constraint_width, strlen( $out[6] ) ); + $this->assertEquals( $constraint_width, strlen( $out[7] ) ); + $this->assertEquals( $constraint_width, strlen( $out[8] ) ); + $this->assertEquals( $constraint_width, strlen( $out[9] ) ); + $this->assertEquals( $constraint_width, strlen( $out[10] ) ); + $this->assertEquals( $constraint_width, strlen( $out[11] ) ); + + $constraint_width = 81; + $renderer = new cli\Table\Ascii; + $renderer->setConstraintWidth( $constraint_width ); + $table->setRenderer( $renderer ); + + $out = $table->getDisplayLines(); + for ( $i = 0; $i < count( $out ); $i++ ) { + $this->assertEquals( $constraint_width, strlen( $out[ $i ] ) ); + } } public function test_column_value_too_long_with_multibytes() { @@ -51,7 +60,18 @@ public function test_column_value_too_long_with_multibytes() { $out = $table->getDisplayLines(); for ( $i = 0; $i < count( $out ); $i++ ) { - $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) + 1 ); + $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); + } + + $constraint_width = 81; + + $renderer = new cli\Table\Ascii; + $renderer->setConstraintWidth( $constraint_width ); + $table->setRenderer( $renderer ); + + $out = $table->getDisplayLines(); + for ( $i = 0; $i < count( $out ); $i++ ) { + $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } } @@ -99,6 +119,61 @@ public function test_column_odd_single_width_with_double_width() { $this->assertSame( 1, count( $result ) ); } + public function test_column_fullwidth_and_combining() { + + $constraint_width = 80; + + $table = new cli\Table; + $renderer = new cli\Table\Ascii; + $renderer->setConstraintWidth( $constraint_width ); + $table->setRenderer( $renderer ); + $table->setHeaders( array( 'Field', 'Value' ) ); + $table->addRow( array( 'ID', 2151 ) ); + $table->addRow( array( 'post_author', 1 ) ); + $table->addRow( array( 'post_title', 'only-english-lorem-ipsum-dolor-sit-amet-consectetur-adipisicing-elit-sed-do-eiusmod-tempor-incididunt-ut-labore' ) ); + $table->addRow( array( 'post_content', + //'ให้รู้จัก ให้หาหนทางใหม่' . + '♫ มีอีกหลายต่อหลายคน เขาอดทนก็เพื่อรัก' . "\n" . + 'รักผลักดันให้รู้จัก ให้หาหนทางใหม่' . "\r\n" . + 'ฉันจะล้มตั้งหลายที ดีที่รักมาฉุดไว้' . "\r\n" . + 'รักสร้างสรรค์สิ่งมากมาย และหลอมละลายทุกหัวใจ' . "\r\n" . + 'จะมาร้ายดียังไง แต่ใจก็ยังต้องการ' . "\r\n" . + 'ในทุกๆ วัน โลกหมุนด้วยความรัก ♫' . "\n" . + 'ขอแสดงความยินดี งานแต่งพี่ Earn & Menn' ."\r\n" . + 'เที่ยวปายหน้าร้อน ก็เที่ยวได้เหมือนกันน่ะ' . "\r\n" . + ' ジョバンニはまっ赤になってうなずきました。けれどもいつかジョバンニの眼のなかには涙がいっぱいになりました。そうだ僕は知っていたのだ、もちろんカムパネルラも知っている。' ."\r\n" . + 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore' . "\n" . + '' + ) ); + + $out = $table->getDisplayLines(); + for ( $i = 0; $i < count( $out ); $i++ ) { + $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); + } + + $constraint_width = 81; + + $renderer = new cli\Table\Ascii; + $renderer->setConstraintWidth( $constraint_width ); + $table->setRenderer( $renderer ); + + $out = $table->getDisplayLines(); + for ( $i = 0; $i < count( $out ); $i++ ) { + $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); + } + + $constraint_width = 200; + + $renderer = new cli\Table\Ascii; + $renderer->setConstraintWidth( $constraint_width ); + $table->setRenderer( $renderer ); + + $out = $table->getDisplayLines(); + for ( $i = 0; $i < count( $out ); $i++ ) { + $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); + } + } + public function test_ascii_pre_colorized_widths() { Colors::enable( true ); From 5591e236435d494edcedaca58f53792f8f6df779 Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 12:29:41 +0100 Subject: [PATCH 19/29] Put stty inside TERM check. Add some php info to travis. --- .travis.yml | 4 ++++ lib/cli/Shell.php | 12 ++++++------ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.travis.yml b/.travis.yml index 889365a..3e48059 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,10 @@ php: - 5.5 - 5.6 +before_script: + - php -m + - php --info | grep -i 'intl\|pcre' + script: phpunit notifications: diff --git a/lib/cli/Shell.php b/lib/cli/Shell.php index 9479c6b..1f49dde 100755 --- a/lib/cli/Shell.php +++ b/lib/cli/Shell.php @@ -50,12 +50,12 @@ static public function columns() { } } else { if ( ! ( $columns = (int) getenv( 'COLUMNS' ) ) ) { - $size = exec( '/usr/bin/env stty size 2>/dev/null' ); - if ( '' !== $size && preg_match( '/[0-9]+ ([0-9]+)/', $size, $matches ) ) { - $columns = (int) $matches[1]; - } - if ( ! $columns ) { - if ( getenv( 'TERM' ) ) { + if ( getenv( 'TERM' ) ) { + $size = exec( '/usr/bin/env stty size 2>/dev/null' ); + if ( '' !== $size && preg_match( '/[0-9]+ ([0-9]+)/', $size, $matches ) ) { + $columns = (int) $matches[1]; + } + if ( ! $columns ) { $columns = (int) exec( '/usr/bin/env tput cols 2>/dev/null' ); } } From 73ef90d7abeae8530a40e5974fcec31b5ef11f60 Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 12:37:34 +0100 Subject: [PATCH 20/29] phpunit --debug --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 3e48059..f213e78 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,7 @@ before_script: - php -m - php --info | grep -i 'intl\|pcre' -script: phpunit +script: phpunit --debug notifications: email: From 9305eddafed81f9d2d983316546fdc48910a0d04 Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 12:42:24 +0100 Subject: [PATCH 21/29] icu info --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index f213e78..5bcf9e6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,13 +2,10 @@ language: php php: - 5.3 - - 5.4 - - 5.5 - - 5.6 before_script: - php -m - - php --info | grep -i 'intl\|pcre' + - php --info | grep -i 'intl\|icu\|pcre' script: phpunit --debug From ceadb8aedfbb60823c65ecfd79c74fcfdbae1446 Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 12:54:13 +0100 Subject: [PATCH 22/29] Disable some table tests. --- tests/test-table.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/test-table.php b/tests/test-table.php index 685a531..342425e 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -34,6 +34,7 @@ public function test_column_value_too_long_ascii() { $this->assertEquals( $constraint_width, strlen( $out[10] ) ); $this->assertEquals( $constraint_width, strlen( $out[11] ) ); + /* $constraint_width = 81; $renderer = new cli\Table\Ascii; @@ -44,6 +45,7 @@ public function test_column_value_too_long_ascii() { for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, strlen( $out[ $i ] ) ); } + */ } public function test_column_value_too_long_with_multibytes() { @@ -63,6 +65,7 @@ public function test_column_value_too_long_with_multibytes() { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } + /* $constraint_width = 81; $renderer = new cli\Table\Ascii; @@ -73,6 +76,7 @@ public function test_column_value_too_long_with_multibytes() { for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } + */ } public function test_column_odd_single_width_with_double_width() { @@ -151,6 +155,7 @@ public function test_column_fullwidth_and_combining() { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } + /* $constraint_width = 81; $renderer = new cli\Table\Ascii; @@ -161,6 +166,7 @@ public function test_column_fullwidth_and_combining() { for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } + */ $constraint_width = 200; From e643eea916773f576d5745206d23dfc53f2a7e80 Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 12:57:11 +0100 Subject: [PATCH 23/29] Disable test_column_value_too_long_with_multibytes. --- tests/test-table.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test-table.php b/tests/test-table.php index 342425e..750834d 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -50,6 +50,7 @@ public function test_column_value_too_long_ascii() { public function test_column_value_too_long_with_multibytes() { + /* $constraint_width = 80; $table = new cli\Table; @@ -65,7 +66,6 @@ public function test_column_value_too_long_with_multibytes() { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } - /* $constraint_width = 81; $renderer = new cli\Table\Ascii; @@ -77,6 +77,7 @@ public function test_column_value_too_long_with_multibytes() { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } */ + $this->assertTrue( true ); } public function test_column_odd_single_width_with_double_width() { From 12633754c1365b1632cedbcbd02235f32f073b14 Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 13:00:17 +0100 Subject: [PATCH 24/29] Disable test_column_fullwidth_and_combining. --- tests/test-table.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test-table.php b/tests/test-table.php index 750834d..5ebc6f7 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -126,6 +126,7 @@ public function test_column_odd_single_width_with_double_width() { public function test_column_fullwidth_and_combining() { + /* $constraint_width = 80; $table = new cli\Table; @@ -156,7 +157,6 @@ public function test_column_fullwidth_and_combining() { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } - /* $constraint_width = 81; $renderer = new cli\Table\Ascii; @@ -167,7 +167,6 @@ public function test_column_fullwidth_and_combining() { for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } - */ $constraint_width = 200; @@ -179,6 +178,8 @@ public function test_column_fullwidth_and_combining() { for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } + */ + $this->assertTrue( true ); } public function test_ascii_pre_colorized_widths() { From bc2eb0675fd35b7ef65b025e2d216c95ec2ff74b Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 13:05:39 +0100 Subject: [PATCH 25/29] Actually disable test_column_fullwidth_and_combining. --- tests/test-table.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test-table.php b/tests/test-table.php index 5ebc6f7..45f86f4 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -82,6 +82,7 @@ public function test_column_value_too_long_with_multibytes() { public function test_column_odd_single_width_with_double_width() { + /* $dummy = new cli\Table; $renderer = new cli\Table\Ascii; @@ -122,6 +123,8 @@ public function test_column_odd_single_width_with_double_width() { $result = $strip_borders( explode( "\n", $out ) ); $this->assertSame( 1, count( $result ) ); + */ + $this->assertTrue( true ); } public function test_column_fullwidth_and_combining() { From bba3559ae929bee5c6f5a8ff28d7c1f0fc848af0 Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 13:35:56 +0100 Subject: [PATCH 26/29] Add can_use_icu(). --- .travis.yml | 1 + lib/cli/cli.php | 25 ++++++++++++++++++++----- tests/test-cli.php | 10 +++++----- 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5bcf9e6..99de78a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,7 @@ language: php php: - 5.3 + - 5.4 before_script: - php -m diff --git a/lib/cli/cli.php b/lib/cli/cli.php index a55f61e..7725ada 100755 --- a/lib/cli/cli.php +++ b/lib/cli/cli.php @@ -165,7 +165,7 @@ function safe_strlen( $str, $encoding = false ) { $test_safe_strlen = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string. - if ( ( ! $encoding || 'UTF-8' === $encoding ) && function_exists( 'grapheme_strlen' ) && null !== ( $length = grapheme_strlen( $str ) ) ) { + if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $length = grapheme_strlen( $str ) ) ) { if ( ! $test_safe_strlen || ( $test_safe_strlen & 1 ) ) { return $length; } @@ -220,7 +220,7 @@ function safe_substr( $str, $start, $length = false, $is_width = false, $encodin $test_safe_substr = getenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR' ); // Assume UTF-8 if no encoding given - `grapheme_substr()` will return false (not null like `grapheme_strlen()`) if given non-UTF-8 string. - if ( ( ! $encoding || 'UTF-8' === $encoding ) && function_exists( 'grapheme_substr' ) && false !== ( $try = grapheme_substr( $str, $start, $length ) ) ) { + if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && false !== ( $try = grapheme_substr( $str, $start, $length ) ) ) { if ( ! $test_safe_substr || ( $test_safe_substr & 1 ) ) { return $is_width ? _safe_substr_eaw( $try, $length ) : $try; } @@ -328,7 +328,7 @@ function strwidth( $string, $encoding = false ) { $test_strwidth = getenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); // Assume UTF-8 if no encoding given - `grapheme_strlen()` will return null if given non-UTF-8 string. - if ( ( ! $encoding || 'UTF-8' === $encoding ) && function_exists( 'grapheme_strlen' ) && null !== ( $width = grapheme_strlen( $string ) ) ) { + if ( ( ! $encoding || 'UTF-8' === $encoding ) && can_use_icu() && null !== ( $width = grapheme_strlen( $string ) ) ) { if ( ! $test_strwidth || ( $test_strwidth & 1 ) ) { return $width + preg_match_all( $eaw_regex, $string, $dummy /*needed for PHP 5.3*/ ); } @@ -356,6 +356,21 @@ function strwidth( $string, $encoding = false ) { return safe_strlen( $string, $encoding ); } +/** + * Returns whether ICU is modern enough not to flake out. + * + * @return bool + */ +function can_use_icu() { + static $can_use_icu = null; + + if ( null === $can_use_icu ) { + $can_use_icu = defined( 'INTL_ICU_VERSION' ) && version_compare( INTL_ICU_VERSION, '49.1', '>=' ) && function_exists( 'grapheme_strlen' ) && function_exists( 'grapheme_substr' ); + } + + return $can_use_icu; +} + /** * Returns whether PCRE Unicode extended grapheme cluster '\X' is available for use. * @@ -367,8 +382,8 @@ function can_use_pcre_x() { if ( null === $can_use_pcre_x ) { // '\X' introduced (as Unicde extended grapheme cluster) in PCRE 8.32 - see https://vcs.pcre.org/pcre/code/tags/pcre-8.32/ChangeLog?view=markup line 53. // Older versions of PCRE were bundled with PHP <= 5.3.23 & <= 5.4.13. - $unfc_pcre_version = substr( PCRE_VERSION, 0, strspn( PCRE_VERSION, '0123456789.' ) ); // Remove any trailing date stuff. - $can_use_pcre_x = version_compare( $unfc_pcre_version, '8.32', '>=' ) && false !== @preg_match( '/\X/u', '' ); + $pcre_version = substr( PCRE_VERSION, 0, strspn( PCRE_VERSION, '0123456789.' ) ); // Remove any trailing date stuff. + $can_use_pcre_x = version_compare( $pcre_version, '8.32', '>=' ) && false !== @preg_match( '/\X/u', '' ); } return $can_use_pcre_x; diff --git a/tests/test-cli.php b/tests/test-cli.php index b404803..bb62532 100644 --- a/tests/test-cli.php +++ b/tests/test-cli.php @@ -104,7 +104,7 @@ function test_various_substr() { // Latin, kana, Latin, Latin combining, Thai combining, Hangul. $str = 'lムnöม้p를'; - if ( function_exists( 'grapheme_substr' ) ) { + if ( \cli\can_use_icu() && function_exists( 'grapheme_substr' ) ) { putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=1' ); // Tests grapheme_substr(). $this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) ); $this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) ); @@ -381,7 +381,7 @@ function test_strwidth() { // 4 characters, one a double-width Han = 5 spacing chars, with 2 combining chars. Adapted from http://unicode.org/faq/char_combmark.html#7 (combining acute accent added after "a"). $str = "a\xCC\x81\xE0\xA4\xA8\xE0\xA4\xBF\xE4\xBA\x9C\xF0\x90\x82\x83"; - if ( function_exists( 'grapheme_strlen' ) ) { + if ( \cli\can_use_icu() && function_exists( 'grapheme_strlen' ) ) { $this->assertSame( 5, \cli\strwidth( $str ) ); // Tests grapheme_strlen(). putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=2' ); // Test preg_match_all( '/\X/u' ). $this->assertSame( 5, \cli\strwidth( $str ) ); @@ -396,7 +396,7 @@ function test_strwidth() { } putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=8' ); // Test safe_strlen(). - if ( function_exists( 'grapheme_strlen' ) || \cli\can_use_pcre_x() ) { + if ( ( \cli\can_use_icu() && function_exists( 'grapheme_strlen' ) ) || \cli\can_use_pcre_x() ) { $this->assertSame( 4, \cli\strwidth( $str ) ); // safe_strlen() (correctly) does not account for double-width Han so out by 1. } elseif ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) { $this->assertSame( 4, \cli\strwidth( $str ) ); // safe_strlen() (correctly) does not account for double-width Han so out by 1. @@ -411,7 +411,7 @@ function test_strwidth() { putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); - if ( function_exists( 'grapheme_strlen' ) ) { + if ( \cli\can_use_icu() && function_exists( 'grapheme_strlen' ) ) { $this->assertSame( 11, \cli\strwidth( $str ) ); // Tests grapheme_strlen(). putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=2' ); // Test preg_match_all( '/\X/u' ). $this->assertSame( 11, \cli\strwidth( $str ) ); @@ -473,7 +473,7 @@ function test_safe_strlen() { // ASCII l, 3-byte kana, ASCII n, ASCII o + 2-byte combining umlaut, 6-byte Thai combining, ASCII, 3-byte Hangul. grapheme length 7, bytes 18. $str = 'lムnöม้p를'; - if ( function_exists( 'grapheme_strlen' ) ) { + if ( \cli\can_use_icu() && function_exists( 'grapheme_strlen' ) ) { putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); // Test grapheme_strlen(). $this->assertSame( 7, \cli\safe_strlen( $str ) ); if ( \cli\can_use_pcre_x() ) { From 8c02e4e68e7774638de307403a9faacc632e71e4 Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 13:41:22 +0100 Subject: [PATCH 27/29] Reenable test_column_value_too_long_with_multibytes. --- .travis.yml | 4 ++++ tests/test-table.php | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 99de78a..24f651d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,10 @@ language: php php: - 5.3 - 5.4 + - 5.5 + - 5.6 + - 7.0 + - 7.1 before_script: - php -m diff --git a/tests/test-table.php b/tests/test-table.php index 45f86f4..a016557 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -34,7 +34,6 @@ public function test_column_value_too_long_ascii() { $this->assertEquals( $constraint_width, strlen( $out[10] ) ); $this->assertEquals( $constraint_width, strlen( $out[11] ) ); - /* $constraint_width = 81; $renderer = new cli\Table\Ascii; @@ -45,12 +44,10 @@ public function test_column_value_too_long_ascii() { for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, strlen( $out[ $i ] ) ); } - */ } public function test_column_value_too_long_with_multibytes() { - /* $constraint_width = 80; $table = new cli\Table; @@ -76,7 +73,6 @@ public function test_column_value_too_long_with_multibytes() { for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } - */ $this->assertTrue( true ); } From 768f836ec5978b3c81337d4cda740fa91c0b63b6 Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 13:54:07 +0100 Subject: [PATCH 28/29] Add phpunit6-compat.php. --- tests/bootstrap.php | 9 ++++++++- tests/phpunit6-compat.php | 19 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 tests/phpunit6-compat.php diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 9c72a0c..31af8b9 100644 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -2,6 +2,13 @@ require dirname( dirname( __FILE__ ) ) . '/lib/cli/cli.php'; +/** + * Compatibility with PHPUnit 6+ + */ +if ( class_exists( 'PHPUnit\Runner\Version' ) ) { + require_once dirname( __FILE__ ) . '/phpunit6-compat.php'; +} + function cli_autoload( $className ) { $className = ltrim($className, '\\'); $fileName = ''; @@ -20,4 +27,4 @@ function cli_autoload( $className ) { require dirname( dirname( __FILE__ ) ) . '/lib/' . $fileName; } -spl_autoload_register( 'cli_autoload' ); \ No newline at end of file +spl_autoload_register( 'cli_autoload' ); diff --git a/tests/phpunit6-compat.php b/tests/phpunit6-compat.php new file mode 100644 index 0000000..73d41cf --- /dev/null +++ b/tests/phpunit6-compat.php @@ -0,0 +1,19 @@ +=' ) ) { + + class_alias( 'PHPUnit\Framework\TestCase', 'PHPUnit_Framework_TestCase' ); + class_alias( 'PHPUnit\Framework\Exception', 'PHPUnit_Framework_Exception' ); + class_alias( 'PHPUnit\Framework\ExpectationFailedException', 'PHPUnit_Framework_ExpectationFailedException' ); + class_alias( 'PHPUnit\Framework\Error\Notice', 'PHPUnit_Framework_Error_Notice' ); + class_alias( 'PHPUnit\Framework\Error\Warning', 'PHPUnit_Framework_Error_Warning' ); + class_alias( 'PHPUnit\Framework\Test', 'PHPUnit_Framework_Test' ); + class_alias( 'PHPUnit\Framework\Warning', 'PHPUnit_Framework_Warning' ); + class_alias( 'PHPUnit\Framework\AssertionFailedError', 'PHPUnit_Framework_AssertionFailedError' ); + class_alias( 'PHPUnit\Framework\TestSuite', 'PHPUnit_Framework_TestSuite' ); + class_alias( 'PHPUnit\Framework\TestListener', 'PHPUnit_Framework_TestListener' ); + class_alias( 'PHPUnit\Util\GlobalState', 'PHPUnit_Util_GlobalState' ); + class_alias( 'PHPUnit\Util\Getopt', 'PHPUnit_Util_Getopt' ); + +} From 61ec37f6e9a49e2cb618ce89a34bf2847431829d Mon Sep 17 00:00:00 2001 From: gitlost Date: Thu, 3 Aug 2017 13:57:39 +0100 Subject: [PATCH 29/29] Re-enable tests. --- tests/test-cli.php | 10 +++++----- tests/test-table.php | 7 ------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/tests/test-cli.php b/tests/test-cli.php index bb62532..6ea4674 100644 --- a/tests/test-cli.php +++ b/tests/test-cli.php @@ -104,7 +104,7 @@ function test_various_substr() { // Latin, kana, Latin, Latin combining, Thai combining, Hangul. $str = 'lムnöม้p를'; - if ( \cli\can_use_icu() && function_exists( 'grapheme_substr' ) ) { + if ( \cli\can_use_icu() ) { putenv( 'PHP_CLI_TOOLS_TEST_SAFE_SUBSTR=1' ); // Tests grapheme_substr(). $this->assertSame( '', \cli\safe_substr( $str, 0, 0 ) ); $this->assertSame( 'l', \cli\safe_substr( $str, 0, 1 ) ); @@ -381,7 +381,7 @@ function test_strwidth() { // 4 characters, one a double-width Han = 5 spacing chars, with 2 combining chars. Adapted from http://unicode.org/faq/char_combmark.html#7 (combining acute accent added after "a"). $str = "a\xCC\x81\xE0\xA4\xA8\xE0\xA4\xBF\xE4\xBA\x9C\xF0\x90\x82\x83"; - if ( \cli\can_use_icu() && function_exists( 'grapheme_strlen' ) ) { + if ( \cli\can_use_icu() ) { $this->assertSame( 5, \cli\strwidth( $str ) ); // Tests grapheme_strlen(). putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=2' ); // Test preg_match_all( '/\X/u' ). $this->assertSame( 5, \cli\strwidth( $str ) ); @@ -396,7 +396,7 @@ function test_strwidth() { } putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=8' ); // Test safe_strlen(). - if ( ( \cli\can_use_icu() && function_exists( 'grapheme_strlen' ) ) || \cli\can_use_pcre_x() ) { + if ( \cli\can_use_icu() || \cli\can_use_pcre_x() ) { $this->assertSame( 4, \cli\strwidth( $str ) ); // safe_strlen() (correctly) does not account for double-width Han so out by 1. } elseif ( function_exists( 'mb_strlen' ) && function_exists( 'mb_detect_order' ) ) { $this->assertSame( 4, \cli\strwidth( $str ) ); // safe_strlen() (correctly) does not account for double-width Han so out by 1. @@ -411,7 +411,7 @@ function test_strwidth() { putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH' ); - if ( \cli\can_use_icu() && function_exists( 'grapheme_strlen' ) ) { + if ( \cli\can_use_icu() ) { $this->assertSame( 11, \cli\strwidth( $str ) ); // Tests grapheme_strlen(). putenv( 'PHP_CLI_TOOLS_TEST_STRWIDTH=2' ); // Test preg_match_all( '/\X/u' ). $this->assertSame( 11, \cli\strwidth( $str ) ); @@ -473,7 +473,7 @@ function test_safe_strlen() { // ASCII l, 3-byte kana, ASCII n, ASCII o + 2-byte combining umlaut, 6-byte Thai combining, ASCII, 3-byte Hangul. grapheme length 7, bytes 18. $str = 'lムnöม้p를'; - if ( \cli\can_use_icu() && function_exists( 'grapheme_strlen' ) ) { + if ( \cli\can_use_icu() ) { putenv( 'PHP_CLI_TOOLS_TEST_SAFE_STRLEN' ); // Test grapheme_strlen(). $this->assertSame( 7, \cli\safe_strlen( $str ) ); if ( \cli\can_use_pcre_x() ) { diff --git a/tests/test-table.php b/tests/test-table.php index a016557..685a531 100644 --- a/tests/test-table.php +++ b/tests/test-table.php @@ -73,12 +73,10 @@ public function test_column_value_too_long_with_multibytes() { for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } - $this->assertTrue( true ); } public function test_column_odd_single_width_with_double_width() { - /* $dummy = new cli\Table; $renderer = new cli\Table\Ascii; @@ -119,13 +117,10 @@ public function test_column_odd_single_width_with_double_width() { $result = $strip_borders( explode( "\n", $out ) ); $this->assertSame( 1, count( $result ) ); - */ - $this->assertTrue( true ); } public function test_column_fullwidth_and_combining() { - /* $constraint_width = 80; $table = new cli\Table; @@ -177,8 +172,6 @@ public function test_column_fullwidth_and_combining() { for ( $i = 0; $i < count( $out ); $i++ ) { $this->assertEquals( $constraint_width, \cli\strwidth( $out[$i] ) ); } - */ - $this->assertTrue( true ); } public function test_ascii_pre_colorized_widths() {