├── .gitignore ├── .idea └── inspectionProfiles │ └── Project_Default.xml ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── Surrogate.cpp ├── TextFlow.hpp ├── TextFlow_Tests.cpp ├── catch.hpp └── main.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/*.iml 2 | .idea/misc.xml 3 | .idea/modules.xml 4 | .idea/vcs.xml 5 | .idea/workspace.xml 6 | .idea/.name 7 | .idea/dictionaries/* 8 | cmake-build-* 9 | .idea 10 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.7) 2 | project(TextFlow) 3 | 4 | set(CMAKE_CXX_STANDARD 11) 5 | 6 | set(SOURCE_FILES main.cpp TextFlow_Tests.cpp TextFlow.hpp Surrogate.cpp) 7 | add_executable(TextFlow ${SOURCE_FILES}) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A simple way to wrap a string at different line lengths, optionally with indents. 2 | 3 | e.g. 4 | 5 | ```c++ 6 | std::cout << Column( "This is quite a long string" ).width( 8 ) << std::endl; 7 | ``` 8 | 9 | will output: 10 | 11 | ``` 12 | This is 13 | quite a 14 | long 15 | string 16 | ``` 17 | 18 | Columns can be combined, too: 19 | 20 | ```c++ 21 | auto a = Column( "This is a load of text that should go on the left" ) 22 | .width( 10 ); 23 | auto b = Column( "Here's some more strings that should be formatted to the right. " 24 | "It's longer so there should be blanks on the left" ) 25 | .width( 12 ) 26 | .initialIndent( 2 ); 27 | 28 | 29 | auto layout = a + Spacer( 4 ) + b; 30 | 31 | std::cout << layout << std::endl; 32 | ``` 33 | 34 | gives you: 35 | 36 | ``` 37 | This is a Here's 38 | load of some more 39 | text that strings that 40 | should go should be 41 | on the formatted to 42 | left the right. 43 | It's longer 44 | so there 45 | should be 46 | blanks on 47 | the left 48 | ``` 49 | 50 | Not bad! 51 | 52 | You can also iterate the lines (which are generated lazily), both from `Column` and the combined `Columns`. 53 | 54 | See the tests for more. -------------------------------------------------------------------------------- /Surrogate.cpp: -------------------------------------------------------------------------------- 1 | // Include the header here to make sure that multiple inclusions don't introduce 2 | // linker errors (e.g. from missing inlines) 3 | #include "TextFlow.hpp" 4 | -------------------------------------------------------------------------------- /TextFlow.hpp: -------------------------------------------------------------------------------- 1 | // TextFlowCpp 2 | // 3 | // A single-header library for wrapping and laying out basic text, by Phil Nash 4 | // 5 | // Distributed under the Boost Software License, Version 1.0. (See accompanying 6 | // file LICENSE.txt or copy at http://www.boost.org/LICENSE_1_0.txt) 7 | // 8 | // This project is hosted at https://github.com/philsquared/textflowcpp 9 | 10 | #ifndef TEXTFLOW_HPP_INCLUDED 11 | #define TEXTFLOW_HPP_INCLUDED 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #ifndef TEXTFLOW_CONFIG_CONSOLE_WIDTH 19 | #define TEXTFLOW_CONFIG_CONSOLE_WIDTH 80 20 | #endif 21 | 22 | 23 | namespace TextFlow { 24 | 25 | inline auto isWhitespace( char c ) -> bool { 26 | static std::string chars = " \t\n\r"; 27 | return chars.find( c ) != std::string::npos; 28 | } 29 | inline auto isBreakableBefore( char c ) -> bool { 30 | static std::string chars = "[({<|"; 31 | return chars.find( c ) != std::string::npos; 32 | } 33 | inline auto isBreakableAfter( char c ) -> bool { 34 | static std::string chars = "])}>.,:;*+-=&/\\"; 35 | return chars.find( c ) != std::string::npos; 36 | } 37 | 38 | class Columns; 39 | 40 | class Column { 41 | std::vector m_strings; 42 | size_t m_width = TEXTFLOW_CONFIG_CONSOLE_WIDTH; 43 | size_t m_indent = 0; 44 | size_t m_initialIndent = std::string::npos; 45 | 46 | public: 47 | class iterator { 48 | friend Column; 49 | 50 | Column const& m_column; 51 | size_t m_stringIndex = 0; 52 | size_t m_pos = 0; 53 | 54 | size_t m_len = 0; 55 | size_t m_end = 0; 56 | bool m_suffix = false; 57 | 58 | iterator( Column const& column, size_t stringIndex ) 59 | : m_column( column ), 60 | m_stringIndex( stringIndex ) 61 | {} 62 | 63 | auto line() const -> std::string const& { return m_column.m_strings[m_stringIndex]; } 64 | 65 | auto isBoundary( size_t at ) const -> bool { 66 | assert( at > 0 ); 67 | assert( at <= line().size() ); 68 | 69 | return at == line().size() || 70 | ( isWhitespace( line()[at] ) && !isWhitespace( line()[at-1] ) ) || 71 | isBreakableBefore( line()[at] ) || 72 | isBreakableAfter( line()[at-1] ); 73 | } 74 | 75 | void calcLength() { 76 | assert( m_stringIndex < m_column.m_strings.size() ); 77 | 78 | m_suffix = false; 79 | auto width = m_column.m_width-indent(); 80 | m_end = m_pos; 81 | if(!line().empty() && line()[m_pos] == '\n') 82 | ++m_end; 83 | while( m_end < line().size() && line()[m_end] != '\n' ) 84 | ++m_end; 85 | 86 | if( m_end < m_pos + width ) { 87 | m_len = m_end - m_pos; 88 | } 89 | else { 90 | size_t len = width; 91 | while (len > 0 && !isBoundary(m_pos + len)) 92 | --len; 93 | while (len > 0 && isWhitespace( line()[m_pos + len - 1] )) 94 | --len; 95 | 96 | if (len > 0) { 97 | m_len = len; 98 | } else { 99 | m_suffix = true; 100 | m_len = width - 1; 101 | } 102 | } 103 | } 104 | 105 | auto indent() const -> size_t { 106 | auto initial = m_pos == 0 && m_stringIndex == 0 ? m_column.m_initialIndent : std::string::npos; 107 | return initial == std::string::npos ? m_column.m_indent : initial; 108 | } 109 | 110 | auto addIndentAndSuffix(std::string const &plain) const -> std::string { 111 | return std::string( indent(), ' ' ) + (m_suffix ? plain + "-" : plain); 112 | } 113 | 114 | public: 115 | using difference_type = std::ptrdiff_t; 116 | using value_type = std::string; 117 | using pointer = value_type*; 118 | using reference = value_type&; 119 | using iterator_category = std::forward_iterator_tag; 120 | 121 | explicit iterator( Column const& column ) : m_column( column ) { 122 | assert( m_column.m_width > m_column.m_indent ); 123 | assert( m_column.m_initialIndent == std::string::npos || m_column.m_width > m_column.m_initialIndent ); 124 | calcLength(); 125 | if( m_len == 0 ) 126 | m_stringIndex++; // Empty string 127 | } 128 | 129 | auto operator *() const -> std::string { 130 | assert( m_stringIndex < m_column.m_strings.size() ); 131 | assert( m_pos <= m_end ); 132 | return addIndentAndSuffix(line().substr(m_pos, m_len)); 133 | } 134 | 135 | auto operator ++() -> iterator& { 136 | m_pos += m_len; 137 | if( m_pos < line().size() && line()[m_pos] == '\n' ) 138 | m_pos += 1; 139 | else 140 | while( m_pos < line().size() && isWhitespace( line()[m_pos] ) ) 141 | ++m_pos; 142 | 143 | if( m_pos == line().size() ) { 144 | m_pos = 0; 145 | ++m_stringIndex; 146 | } 147 | if( m_stringIndex < m_column.m_strings.size() ) 148 | calcLength(); 149 | return *this; 150 | } 151 | auto operator ++(int) -> iterator { 152 | iterator prev( *this ); 153 | operator++(); 154 | return prev; 155 | } 156 | 157 | auto operator ==( iterator const& other ) const -> bool { 158 | return 159 | m_pos == other.m_pos && 160 | m_stringIndex == other.m_stringIndex && 161 | &m_column == &other.m_column; 162 | } 163 | auto operator !=( iterator const& other ) const -> bool { 164 | return !operator==( other ); 165 | } 166 | }; 167 | using const_iterator = iterator; 168 | 169 | explicit Column( std::string const& text ) { m_strings.push_back( text ); } 170 | 171 | auto width( size_t newWidth ) -> Column& { 172 | assert( newWidth > 0 ); 173 | m_width = newWidth; 174 | return *this; 175 | } 176 | auto indent( size_t newIndent ) -> Column& { 177 | m_indent = newIndent; 178 | return *this; 179 | } 180 | auto initialIndent( size_t newIndent ) -> Column& { 181 | m_initialIndent = newIndent; 182 | return *this; 183 | } 184 | 185 | auto width() const -> size_t { return m_width; } 186 | auto begin() const -> iterator { return iterator( *this ); } 187 | auto end() const -> iterator { return { *this, m_strings.size() }; } 188 | 189 | inline friend std::ostream& operator << ( std::ostream& os, Column const& col ) { 190 | bool first = true; 191 | for( auto line : col ) { 192 | if( first ) 193 | first = false; 194 | else 195 | os << "\n"; 196 | os << line; 197 | } 198 | return os; 199 | } 200 | 201 | auto operator + ( Column const& other ) -> Columns; 202 | 203 | auto toString() const -> std::string { 204 | std::ostringstream oss; 205 | oss << *this; 206 | return oss.str(); 207 | } 208 | }; 209 | 210 | class Spacer : public Column { 211 | 212 | public: 213 | explicit Spacer( size_t spaceWidth ) : Column( "" ) { 214 | width( spaceWidth ); 215 | } 216 | }; 217 | 218 | class Columns { 219 | std::vector m_columns; 220 | 221 | public: 222 | 223 | class iterator { 224 | friend Columns; 225 | struct EndTag {}; 226 | 227 | std::vector const& m_columns; 228 | std::vector m_iterators; 229 | size_t m_activeIterators; 230 | 231 | iterator( Columns const& columns, EndTag ) 232 | : m_columns( columns.m_columns ), 233 | m_activeIterators( 0 ) 234 | { 235 | m_iterators.reserve( m_columns.size() ); 236 | 237 | for( auto const& col : m_columns ) 238 | m_iterators.push_back( col.end() ); 239 | } 240 | 241 | public: 242 | using difference_type = std::ptrdiff_t; 243 | using value_type = std::string; 244 | using pointer = value_type*; 245 | using reference = value_type&; 246 | using iterator_category = std::forward_iterator_tag; 247 | 248 | explicit iterator( Columns const& columns ) 249 | : m_columns( columns.m_columns ), 250 | m_activeIterators( m_columns.size() ) 251 | { 252 | m_iterators.reserve( m_columns.size() ); 253 | 254 | for( auto const& col : m_columns ) 255 | m_iterators.push_back( col.begin() ); 256 | } 257 | 258 | auto operator ==( iterator const& other ) const -> bool { 259 | return m_iterators == other.m_iterators; 260 | } 261 | auto operator !=( iterator const& other ) const -> bool { 262 | return m_iterators != other.m_iterators; 263 | } 264 | auto operator *() const -> std::string { 265 | std::string row, padding; 266 | 267 | for( size_t i = 0; i < m_columns.size(); ++i ) { 268 | auto width = m_columns[i].width(); 269 | if( m_iterators[i] != m_columns[i].end() ) { 270 | std::string col = *m_iterators[i]; 271 | row += padding + col; 272 | if( col.size() < width ) 273 | padding = std::string( width - col.size(), ' ' ); 274 | else 275 | padding = ""; 276 | } 277 | else { 278 | padding += std::string( width, ' ' ); 279 | } 280 | } 281 | return row; 282 | } 283 | auto operator ++() -> iterator& { 284 | for( size_t i = 0; i < m_columns.size(); ++i ) { 285 | if (m_iterators[i] != m_columns[i].end()) 286 | ++m_iterators[i]; 287 | } 288 | return *this; 289 | } 290 | auto operator ++(int) -> iterator { 291 | iterator prev( *this ); 292 | operator++(); 293 | return prev; 294 | } 295 | }; 296 | using const_iterator = iterator; 297 | 298 | auto begin() const -> iterator { return iterator( *this ); } 299 | auto end() const -> iterator { return { *this, iterator::EndTag() }; } 300 | 301 | auto operator += ( Column const& col ) -> Columns& { 302 | m_columns.push_back( col ); 303 | return *this; 304 | } 305 | auto operator + ( Column const& col ) -> Columns { 306 | Columns combined = *this; 307 | combined += col; 308 | return combined; 309 | } 310 | 311 | inline friend std::ostream& operator << ( std::ostream& os, Columns const& cols ) { 312 | 313 | bool first = true; 314 | for( auto line : cols ) { 315 | if( first ) 316 | first = false; 317 | else 318 | os << "\n"; 319 | os << line; 320 | } 321 | return os; 322 | } 323 | 324 | auto toString() const -> std::string { 325 | std::ostringstream oss; 326 | oss << *this; 327 | return oss.str(); 328 | } 329 | }; 330 | 331 | inline auto Column::operator + ( Column const& other ) -> Columns { 332 | Columns cols; 333 | cols += *this; 334 | cols += other; 335 | return cols; 336 | } 337 | } 338 | 339 | #endif // TEXTFLOW_HPP_INCLUDED 340 | -------------------------------------------------------------------------------- /TextFlow_Tests.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "TextFlow.hpp" 3 | 4 | #include "catch.hpp" 5 | 6 | using namespace TextFlow; 7 | 8 | TEST_CASE( "short string" ) { 9 | Column col( "short string" ); 10 | 11 | auto it = col.begin(); 12 | auto itEnd = col.end(); 13 | CHECK( *it == "short string" ); 14 | CHECK( it != itEnd ); 15 | it++; 16 | CHECK( it == itEnd ); 17 | 18 | CHECK( col.toString() == "short string" ); 19 | } 20 | 21 | TEST_CASE( "long string" ) { 22 | Column col( "The quick brown fox jumped over the lazy dog" ); 23 | 24 | // Use various column widths 25 | SECTION("7") { 26 | col.width(7); 27 | CHECK( col.toString() == 28 | "The\n" 29 | "quick\n" 30 | "brown\n" 31 | "fox\n" 32 | "jumped\n" 33 | "over\n" 34 | "the\n" 35 | "lazy\n" 36 | "dog" ); 37 | } 38 | SECTION("8") { 39 | col.width(8); 40 | CHECK( col.toString() == 41 | "The\n" 42 | "quick\n" 43 | "brown\n" 44 | "fox\n" 45 | "jumped\n" 46 | "over the\n" 47 | "lazy dog" ); 48 | } 49 | SECTION("9") { 50 | col.width(9); 51 | CHECK( col.toString() == 52 | "The quick\n" 53 | "brown fox\n" 54 | "jumped\n" 55 | "over the\n" 56 | "lazy dog" ); 57 | } 58 | SECTION("10") { 59 | col.width(10); 60 | CHECK( col.toString() == 61 | "The quick\n" 62 | "brown fox\n" 63 | "jumped\n" 64 | "over the\n" 65 | "lazy dog" ); 66 | } 67 | SECTION("11") { 68 | col.width(11); 69 | CHECK( col.toString() == 70 | "The quick\n" 71 | "brown fox\n" 72 | "jumped over\n" 73 | "the lazy\n" 74 | "dog" ); 75 | } 76 | SECTION("12") { 77 | col.width(12); 78 | CHECK( col.toString() == 79 | "The quick\n" 80 | "brown fox\n" 81 | "jumped over\n" 82 | "the lazy dog" ); 83 | } 84 | SECTION("20") { 85 | col.width(20); 86 | CHECK( col.toString() == 87 | "The quick brown fox\n" 88 | "jumped over the lazy\n" 89 | "dog" ); 90 | } 91 | } 92 | TEST_CASE( "edge cases" ) { 93 | SECTION( "empty string" ) 94 | CHECK( Column("").toString() == "" ); 95 | SECTION( "space" ) 96 | CHECK( Column(" ").toString() == " " ); 97 | SECTION( "char with trailing space" ) 98 | CHECK( Column("a ").toString() == "a " ); 99 | SECTION( "long string with trailing space" ) 100 | CHECK( Column("once upon a time ").width(10).toString() == "once upon\na time " ); 101 | 102 | // can't check these as they assert, currently 103 | 104 | // SECTION( "indent > width" ) 105 | // CHECK_THROWS( Column("test").width(10).indent(10).toString() ); 106 | // SECTION( "initialIndent > width" ) 107 | // CHECK_THROWS( Column("test").width(10).initialIndent(10).toString() ); 108 | 109 | // SECTION( "zero width" ) 110 | // CHECK_THROWS( Column("hello").width(0).toString() ); 111 | } 112 | 113 | TEST_CASE( "unbreakable" ) { 114 | auto col = Column("unbreakable").width(8); 115 | CHECK( col.toString() == "unbreak-\nable"); 116 | } 117 | 118 | TEST_CASE( "wrap points" ) { 119 | 120 | SECTION( "round brackets" ) { 121 | auto col = Column( "(hello)aaa(world)" ); 122 | 123 | SECTION("8") 124 | CHECK(col.width(8).toString() == "(hello)\naaa\n(world)"); 125 | SECTION("10") 126 | CHECK(col.width(10).toString() == "(hello)aaa\n(world)"); 127 | SECTION("11") 128 | CHECK(col.width(11).toString() == "(hello)aaa\n(world)"); 129 | SECTION("18") 130 | CHECK(col.width(18).toString() == "(hello)aaa(world)"); 131 | } 132 | 133 | // These tests come from the original Catch tests 134 | SECTION( "Mixed" ) { 135 | Column col( "one,two(three) " ); 136 | 137 | SECTION("Wrap before") { 138 | CHECK( col.width(11).toString() == "one,two\n(three)\n" ); 139 | } 140 | SECTION("Wrap after") { 141 | CHECK( col.width(6).toString() == "one,\ntwo\n(thre-\ne)\n" ); 142 | CHECK( col.width(5).toString() == "one,\ntwo\n(thr-\nee)\n"); 143 | CHECK( col.width(4).toString() == "one,\ntwo\n(th-\nree)\n"); 144 | } 145 | } 146 | } 147 | 148 | auto toVector ( Column const& col ) -> std::vector { 149 | std::vector lines; 150 | std::copy( col.begin(), col.end(), std::back_inserter( lines ) ); 151 | return lines; 152 | } 153 | auto toVector ( Columns const& cols ) -> std::vector { 154 | std::vector lines; 155 | std::copy( cols.begin(), cols.end(), std::back_inserter( lines ) ); 156 | return lines; 157 | } 158 | 159 | TEST_CASE( "indents" ) { 160 | auto col = Column( 161 | "It is a period of civil war. " 162 | "Rebel spaceships, striking from a hidden base, have won their first victory against the evil Galactic Empire. " 163 | "During the battle, Rebel spies managed to steal secret plans to the Empire’s ultimate weapon, the DEATH STAR, an armored space station with enough power to destroy an entire planet. \n" 164 | "Pursued by the Empire’s sinister agents, Princess Leia races home aboard her starship, custodian of the stolen plans that can save her people and restore freedom to the galaxy..." 165 | ).width(40); 166 | 167 | SECTION( "no indent" ) { 168 | auto lines = toVector(col); 169 | 170 | REQUIRE(lines[0] == "It is a period of civil war. Rebel"); 171 | REQUIRE(lines[1] == "spaceships, striking from a hidden base,"); 172 | REQUIRE(lines[2] == "have won their first victory against the"); 173 | REQUIRE(lines[3] == "evil Galactic Empire. During the battle,"); 174 | } 175 | 176 | SECTION( "indent only" ) { 177 | col.indent( 4 ); 178 | auto lines = toVector(col); 179 | 180 | // 0123456789012345678901234567890123456789 181 | REQUIRE(lines[0] == " It is a period of civil war. Rebel"); 182 | REQUIRE(lines[1] == " spaceships, striking from a hidden"); 183 | REQUIRE(lines[2] == " base, have won their first victory"); 184 | REQUIRE(lines[3] == " against the evil Galactic Empire." ); 185 | } 186 | SECTION( "initial indent only" ) { 187 | col.initialIndent( 8 ); 188 | auto lines = toVector(col); 189 | 190 | // 0123456789012345678901234567890123456789 191 | REQUIRE(lines[0] == " It is a period of civil war."); 192 | REQUIRE(lines[1] == "Rebel spaceships, striking from a hidden"); 193 | REQUIRE(lines[2] == "base, have won their first victory"); 194 | REQUIRE(lines[3] == "against the evil Galactic Empire. During" ); 195 | } 196 | SECTION( "initial indent > indent" ) { 197 | col.initialIndent( 8 ).indent( 4 ); 198 | auto lines = toVector(col); 199 | 200 | // 0123456789012345678901234567890123456789 201 | REQUIRE(lines[0] == " It is a period of civil war."); 202 | REQUIRE(lines[1] == " Rebel spaceships, striking from a"); 203 | REQUIRE(lines[2] == " hidden base, have won their first"); 204 | REQUIRE(lines[3] == " victory against the evil Galactic" ); 205 | } 206 | SECTION( "initial indent < indent" ) { 207 | col.initialIndent( 4 ).indent( 8 ); 208 | auto lines = toVector(col); 209 | 210 | // 0123456789012345678901234567890123456789 211 | REQUIRE(lines[0] == " It is a period of civil war. Rebel"); 212 | REQUIRE(lines[1] == " spaceships, striking from a"); 213 | REQUIRE(lines[2] == " hidden base, have won their"); 214 | REQUIRE(lines[3] == " first victory against the evil" ); 215 | } 216 | } 217 | 218 | TEST_CASE( "combined columns" ) { 219 | 220 | auto a = Column( "This is a load of text that should go on the left" ).width(10); 221 | auto b = Column( "Here's some more strings that should be formatted to the right. " 222 | "It's longer so there should be blanks on the left" ).width(12); 223 | 224 | auto layout = a + Spacer(4) + b; 225 | 226 | 227 | auto lines = toVector( layout ); 228 | CHECK( lines[0] == "This is a Here's some" ); 229 | CHECK( lines[1] == "load of more strings" ); 230 | CHECK( lines[2] == "text that that should" ); 231 | CHECK( lines[6] == " longer so" ); 232 | 233 | CHECK( layout.toString() == 234 | "This is a Here's some\n" 235 | "load of more strings\n" 236 | "text that that should\n" 237 | "should go be formatted\n" 238 | "on the to the\n" 239 | "left right. It's\n" 240 | " longer so\n" 241 | " there should\n" 242 | " be blanks on\n" 243 | " the left" ); 244 | 245 | } 246 | 247 | TEST_CASE( "indent at existing newlines" ) { 248 | auto col = Column( "This text has\n newlines\nembedded in it - but also some long text that should be wrapped" ) 249 | .width(20) 250 | .indent(2); 251 | 252 | REQUIRE( col.toString() == 253 | " This text has\n" 254 | " newlines\n" 255 | " embedded in it -\n" 256 | " but also some long\n" 257 | " text that should\n" 258 | " be wrapped" ); 259 | } 260 | 261 | TEST_CASE( "another long string" ) { 262 | // This test is taken from @JoeyGrajciar's PR against the Clara repo: 263 | 264 | // https://github.com/catchorg/Clara/pull/74 265 | 266 | const auto long_string = std::string( 267 | "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque nisl \n" 268 | "massa, luctus ut ligula vitae, suscipit tempus velit. Vivamus sodales, quam in \n" 269 | "convallis posuere, libero nisi ultricies orci, nec lobortis.\n"); 270 | 271 | auto col = Column( long_string ) 272 | .width(79) 273 | .indent(2); 274 | 275 | REQUIRE( col.toString() == 276 | " Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque nisl \n" 277 | " massa, luctus ut ligula vitae, suscipit tempus velit. Vivamus sodales, quam\n" 278 | " in \n" 279 | " convallis posuere, libero nisi ultricies orci, nec lobortis." ); 280 | } 281 | 282 | TEST_CASE( "message with linebreak at start" ) 283 | { 284 | const auto message = std::string( 285 | "\nthis is a message starting with linebreak" 286 | ); 287 | 288 | auto col = Column( message ) 289 | .width(79) 290 | .indent(2); 291 | 292 | REQUIRE(col.toString() == 293 | " \n" 294 | "this is a message starting with linebreak" 295 | ); 296 | } 297 | 298 | std::mt19937 rng; 299 | std::uniform_int_distribution wordCharGenerator(33,126); 300 | std::uniform_int_distribution wsGenerator(0, 11); 301 | std::uniform_int_distribution wordLenGenerator(1, 10); 302 | std::uniform_int_distribution wsLenGenerator(1, 2); 303 | std::uniform_int_distribution punctGenerator(0, 40); 304 | 305 | auto randomWordChar() -> char { 306 | return static_cast( wordCharGenerator(rng) ); 307 | } 308 | auto randomWS() -> char { 309 | static char const* ws = " \t\r\n"; 310 | return ( ws[wsGenerator(rng)] ); 311 | } 312 | auto generateWord( int length ) -> std::string { 313 | std::string s; 314 | for(; length > 0; --length ) 315 | s += randomWordChar(); 316 | return s; 317 | } 318 | auto generateWord() -> std::string { 319 | return generateWord( wordLenGenerator(rng) ); 320 | } 321 | auto generateWS( int length ) -> std::string { 322 | std::string s; 323 | for(; length > 0; --length ) 324 | s += randomWS(); 325 | return s; 326 | } 327 | auto generateText( int words ) -> std::string { 328 | std::string text; 329 | for(; words > 0; --words ) { 330 | text += generateWord(); 331 | switch( punctGenerator(rng) ) 332 | { 333 | case 1: 334 | case 2: 335 | text += ","; 336 | break; 337 | case 3: 338 | case 4: 339 | text += "."; 340 | break; 341 | } 342 | text += generateWS( wsLenGenerator(rng) ); 343 | } 344 | return text; 345 | } 346 | 347 | TEST_CASE( "randomly generated text", "[.]" ) { 348 | for( int j = 0; j < 5; ++j ) { 349 | for(int i = 1; i < 200; ++i ) { 350 | auto s = generateText( i ); 351 | auto size = s.size(); 352 | for( int c = 3; c < s.size(); ++c ) { 353 | CAPTURE( j ); 354 | CAPTURE( i ); 355 | CAPTURE( c ); 356 | CAPTURE( s ); 357 | INFO( Column( s ).width(c).toString() ); 358 | } 359 | } 360 | } 361 | SUCCEED(); 362 | } 363 | -------------------------------------------------------------------------------- /main.cpp: -------------------------------------------------------------------------------- 1 | #define CATCH_CONFIG_MAIN 2 | #include "catch.hpp" 3 | --------------------------------------------------------------------------------