// Copyright Catch2 Authors // Distributed under the Boost Software License, Version 1.0. // (See accompanying file LICENSE.txt or copy at // https://www.boost.org/LICENSE_1_0.txt) // SPDX-License-Identifier: BSL-1.0 #include #include #include #include namespace { bool isWhitespace( char c ) { return c == ' ' || c == '\t' || c == '\n' || c == '\r'; } bool isBreakableBefore( char c ) { static const char chars[] = "[({<|"; return std::memchr( chars, c, sizeof( chars ) - 1 ) != nullptr; } bool isBreakableAfter( char c ) { static const char chars[] = "])}>.,:;*+-=&/\\"; return std::memchr( chars, c, sizeof( chars ) - 1 ) != nullptr; } } // namespace namespace Catch { namespace TextFlow { void AnsiSkippingString::preprocessString() { for ( auto it = m_string.begin(); it != m_string.end(); ) { // try to read through an ansi sequence while ( it != m_string.end() && *it == '\033' && it + 1 != m_string.end() && *( it + 1 ) == '[' ) { auto cursor = it + 2; while ( cursor != m_string.end() && ( isdigit( *cursor ) || *cursor == ';' ) ) { ++cursor; } if ( cursor == m_string.end() || *cursor != 'm' ) { break; } // 'm' -> 0xff *cursor = AnsiSkippingString::sentinel; // if we've read an ansi sequence, set the iterator and // return to the top of the loop it = cursor + 1; } if ( it != m_string.end() ) { ++m_size; ++it; } } } AnsiSkippingString::AnsiSkippingString( std::string const& text ): m_string( text ) { preprocessString(); } AnsiSkippingString::AnsiSkippingString( std::string&& text ): m_string( CATCH_MOVE( text ) ) { preprocessString(); } AnsiSkippingString::const_iterator AnsiSkippingString::begin() const { return const_iterator( m_string ); } AnsiSkippingString::const_iterator AnsiSkippingString::end() const { return const_iterator( m_string, const_iterator::EndTag{} ); } std::string AnsiSkippingString::substring( const_iterator begin, const_iterator end ) const { // There's one caveat here to an otherwise simple substring: when // making a begin iterator we might have skipped ansi sequences at // the start. If `begin` here is a begin iterator, skipped over // initial ansi sequences, we'll use the true beginning of the // string. Lastly: We need to transform any chars we replaced with // 0xff back to 'm' auto str = std::string( begin == this->begin() ? m_string.begin() : begin.m_it, end.m_it ); std::transform( str.begin(), str.end(), str.begin(), []( char c ) { return c == AnsiSkippingString::sentinel ? 'm' : c; } ); return str; } void AnsiSkippingString::const_iterator::tryParseAnsiEscapes() { // check if we've landed on an ansi sequence, and if so read through // it while ( m_it != m_string->end() && *m_it == '\033' && m_it + 1 != m_string->end() && *( m_it + 1 ) == '[' ) { auto cursor = m_it + 2; while ( cursor != m_string->end() && ( isdigit( *cursor ) || *cursor == ';' ) ) { ++cursor; } if ( cursor == m_string->end() || *cursor != AnsiSkippingString::sentinel ) { break; } // if we've read an ansi sequence, set the iterator and // return to the top of the loop m_it = cursor + 1; } } void AnsiSkippingString::const_iterator::advance() { assert( m_it != m_string->end() ); m_it++; tryParseAnsiEscapes(); } void AnsiSkippingString::const_iterator::unadvance() { assert( m_it != m_string->begin() ); m_it--; // if *m_it is 0xff, scan back to the \033 and then m_it-- once more // (and repeat check) while ( *m_it == AnsiSkippingString::sentinel ) { while ( *m_it != '\033' ) { assert( m_it != m_string->begin() ); m_it--; } // if this happens, we must have been a begin iterator that had // skipped over ansi sequences at the start of a string assert( m_it != m_string->begin() ); assert( *m_it == '\033' ); m_it--; } } static bool isBoundary( AnsiSkippingString const& line, AnsiSkippingString::const_iterator it ) { return it == line.end() || ( isWhitespace( *it ) && !isWhitespace( *it.oneBefore() ) ) || isBreakableBefore( *it ) || isBreakableAfter( *it.oneBefore() ); } void Column::const_iterator::calcLength() { m_addHyphen = false; m_parsedTo = m_lineStart; AnsiSkippingString const& current_line = m_column.m_string; if ( m_parsedTo == current_line.end() ) { m_lineEnd = m_parsedTo; return; } assert( m_lineStart != current_line.end() ); if ( *m_lineStart == '\n' ) { ++m_parsedTo; } const auto maxLineLength = m_column.m_width - indentSize(); std::size_t lineLength = 0; while ( m_parsedTo != current_line.end() && lineLength < maxLineLength && *m_parsedTo != '\n' ) { ++m_parsedTo; ++lineLength; } // If we encountered a newline before the column is filled, // then we linebreak at the newline and consider this line // finished. if ( lineLength < maxLineLength ) { m_lineEnd = m_parsedTo; } else { // Look for a natural linebreak boundary in the column // (We look from the end, so that the first found boundary is // the right one) m_lineEnd = m_parsedTo; while ( lineLength > 0 && !isBoundary( current_line, m_lineEnd ) ) { --lineLength; --m_lineEnd; } while ( lineLength > 0 && isWhitespace( *m_lineEnd.oneBefore() ) ) { --lineLength; --m_lineEnd; } // If we found one, then that is where we linebreak, otherwise // we have to split text with a hyphen if ( lineLength == 0 ) { m_addHyphen = true; m_lineEnd = m_parsedTo.oneBefore(); } } } size_t Column::const_iterator::indentSize() const { auto initial = m_lineStart == m_column.m_string.begin() ? m_column.m_initialIndent : std::string::npos; return initial == std::string::npos ? m_column.m_indent : initial; } std::string Column::const_iterator::addIndentAndSuffix( AnsiSkippingString::const_iterator start, AnsiSkippingString::const_iterator end ) const { std::string ret; const auto desired_indent = indentSize(); // ret.reserve( desired_indent + (end - start) + m_addHyphen ); ret.append( desired_indent, ' ' ); // ret.append( start, end ); ret += m_column.m_string.substring( start, end ); if ( m_addHyphen ) { ret.push_back( '-' ); } return ret; } Column::const_iterator::const_iterator( Column const& column ): m_column( column ), m_lineStart( column.m_string.begin() ), m_lineEnd( column.m_string.begin() ), m_parsedTo( column.m_string.begin() ) { assert( m_column.m_width > m_column.m_indent ); assert( m_column.m_initialIndent == std::string::npos || m_column.m_width > m_column.m_initialIndent ); calcLength(); if ( m_lineStart == m_lineEnd ) { m_lineStart = m_column.m_string.end(); } } std::string Column::const_iterator::operator*() const { assert( m_lineStart <= m_parsedTo ); return addIndentAndSuffix( m_lineStart, m_lineEnd ); } Column::const_iterator& Column::const_iterator::operator++() { m_lineStart = m_lineEnd; AnsiSkippingString const& current_line = m_column.m_string; if ( m_lineStart != current_line.end() && *m_lineStart == '\n' ) { m_lineStart++; } else { while ( m_lineStart != current_line.end() && isWhitespace( *m_lineStart ) ) { ++m_lineStart; } } if ( m_lineStart != current_line.end() ) { calcLength(); } return *this; } Column::const_iterator Column::const_iterator::operator++( int ) { const_iterator prev( *this ); operator++(); return prev; } std::ostream& operator<<( std::ostream& os, Column const& col ) { bool first = true; for ( auto line : col ) { if ( first ) { first = false; } else { os << '\n'; } os << line; } return os; } Column Spacer( size_t spaceWidth ) { Column ret{ "" }; ret.width( spaceWidth ); return ret; } Columns::iterator::iterator( Columns const& columns, EndTag ): m_columns( columns.m_columns ), m_activeIterators( 0 ) { m_iterators.reserve( m_columns.size() ); for ( auto const& col : m_columns ) { m_iterators.push_back( col.end() ); } } Columns::iterator::iterator( Columns const& columns ): m_columns( columns.m_columns ), m_activeIterators( m_columns.size() ) { m_iterators.reserve( m_columns.size() ); for ( auto const& col : m_columns ) { m_iterators.push_back( col.begin() ); } } std::string Columns::iterator::operator*() const { std::string row, padding; for ( size_t i = 0; i < m_columns.size(); ++i ) { const auto width = m_columns[i].width(); if ( m_iterators[i] != m_columns[i].end() ) { std::string col = *m_iterators[i]; row += padding; row += col; padding.clear(); if ( col.size() < width ) { padding.append( width - col.size(), ' ' ); } } else { padding.append( width, ' ' ); } } return row; } Columns::iterator& Columns::iterator::operator++() { for ( size_t i = 0; i < m_columns.size(); ++i ) { if ( m_iterators[i] != m_columns[i].end() ) { ++m_iterators[i]; } } return *this; } Columns::iterator Columns::iterator::operator++( int ) { iterator prev( *this ); operator++(); return prev; } std::ostream& operator<<( std::ostream& os, Columns const& cols ) { bool first = true; for ( auto line : cols ) { if ( first ) { first = false; } else { os << '\n'; } os << line; } return os; } Columns operator+( Column const& lhs, Column const& rhs ) { Columns cols; cols += lhs; cols += rhs; return cols; } Columns operator+( Column&& lhs, Column&& rhs ) { Columns cols; cols += CATCH_MOVE( lhs ); cols += CATCH_MOVE( rhs ); return cols; } Columns& operator+=( Columns& lhs, Column const& rhs ) { lhs.m_columns.push_back( rhs ); return lhs; } Columns& operator+=( Columns& lhs, Column&& rhs ) { lhs.m_columns.push_back( CATCH_MOVE( rhs ) ); return lhs; } Columns operator+( Columns const& lhs, Column const& rhs ) { auto combined( lhs ); combined += rhs; return combined; } Columns operator+( Columns&& lhs, Column&& rhs ) { lhs += CATCH_MOVE( rhs ); return CATCH_MOVE( lhs ); } } // namespace TextFlow } // namespace Catch