Lector v1.0.0
C++ library for parsing command line arguments.
text.hpp
1// Copyright © 2026, Alexandre Coderre-Chabot.
2
3// This file is part of Lector (https://github.com/acodcha/lector), a C++ library for parsing
4// command line arguments. Lector is licensed under the MIT License (https://mit-license.org).
5
6// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
7// associated documentation files (the "Software"), to deal in the Software without restriction,
8// including without limitation the rights to use, copy, modify, merge, publish, distribute,
9// sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
10// furnished to do so, subject to the following conditions:
11// - The above copyright notice and this permission notice shall be included in all copies or
12// substantial portions of the Software.
13// - THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
14// BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
15// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
16// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM
17// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18
19#ifndef LECTOR_TEXT_HPP
20#define LECTOR_TEXT_HPP
21
22#include <algorithm>
23#include <cctype>
24#include <cstddef>
25#include <stdexcept>
26#include <string>
27#include <string_view>
28#include <utility>
29#include <vector>
30
31/// @brief The Lector library's namespace.
32namespace lector {
33
34/// @brief Returns whether a given character is the leading byte of a UTF-8 character. All UTF-8
35/// characters measure either one, two, three, or four bytes. UTF-8 characters that measure only one
36/// byte are the ASCII characters. UTF-8 character that measure two, three, or four bytes are
37/// multi-byte characters and consist of a leading byte with a specific binary pattern and one or
38/// more continuation bytes of the binary pattern 10xxxxxx.
39///
40/// 1. One-byte UTF-8 characters are the ASCII characters. Their first bit is 0 and their binary
41/// pattern is therefore 0xxxxxxx.
42///
43/// 2. Two-byte UTF-8 characters have a leading byte with the binary pattern 110xxxxx and one
44/// continuation byte with the binary pattern 10xxxxxx. Together, the two bytes therefore have
45/// the binary pattern 110xxxxx 10xxxxxx.
46///
47/// 3. Three-byte UTF-8 characters have a leading byte with the binary pattern 1110xxxx and two
48/// continuation bytes with the binary pattern 10xxxxxx. Together, the three bytes therefore have
49/// the binary pattern 1110xxxx 10xxxxxx 10xxxxxx.
50///
51/// 4. Four-byte UTF-8 characters have a leading byte with the binary pattern 11110xxx and three
52/// continuation bytes with the binary pattern 10xxxxxx. Together, the four bytes therefore have
53/// the binary pattern 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx.
54/// @param[in] character The character to check.
55/// @return True if the character is a leading byte; false if the character is a continuation byte.
56[[nodiscard]] inline bool is_leading_byte(const char character) {
57 // Cast to an unsigned character to avoid undefined behavior with bitwise operations on signed
58 // characters. The binary pattern 10xxxxxx that identifies a continuation byte ranges from 0x80 to
59 // 0xBF in hexadecimal notation.
60 return (static_cast<unsigned char>(character) & static_cast<unsigned char>(0xC0))
61 != static_cast<unsigned char>(0x80);
62}
63
64/// @brief Finds the exact byte [begin, end) index interval in a string of text where a specified
65/// code point resides.
66/// @param[in] text The string of text to parse.
67/// @param[in] code_point_index The index of the code point in the string of text.
68/// @return A pair that contains the begin and end byte indices of the specified code point. The end
69/// index is the classical C++ "one past the end" index. If the specified code point index is out of
70/// bounds, both returned indices are set to one past the end index of the string, which is the size
71/// of the string.
72[[nodiscard]] inline std::pair<std::size_t, std::size_t> byte_interval(
73 const std::string_view text, const std::size_t code_point_index) {
74 std::size_t current_code_point_index{0UL};
75 std::size_t begin_byte_index{text.size()};
76 for (std::size_t current_byte_index{0UL}; current_byte_index < text.size();
77 ++current_byte_index) {
78 if (lector::is_leading_byte(text.at(current_byte_index))) {
79 if (current_code_point_index == code_point_index) {
80 begin_byte_index = current_byte_index;
81 } else if (current_code_point_index == code_point_index + static_cast<std::size_t>(1UL)) {
82 // In this case, this is the start of the next code point, and therefore the end of the
83 // requested code point.
84 return std::pair<std::size_t, std::size_t>{begin_byte_index, current_byte_index};
85 }
86 ++current_code_point_index;
87 }
88 }
89 if (begin_byte_index < text.size()) {
90 // In this case, the requested code point is found, but it is the last code point in the string.
91 return std::pair<std::size_t, std::size_t>{begin_byte_index, text.size()};
92 }
93 // In this case, the requested code point index is out of bounds.
94 return std::pair<std::size_t, std::size_t>{text.size(), text.size()};
95}
96
97/// @brief Counts and returns the number of UTF-8 code points in a string of text. The number of
98/// UTF-8 code points is a useful approximation of the number of graphemes in the string, where
99/// ASCII characters and multi-byte UTF-8 characters are each counted as one unit of length.
100/// @param[in] text The string of text whose UTF-8 code points are to be counted.
101/// @return The number of UTF-8 code points in the string of text.
102[[nodiscard]] inline std::size_t code_points(const std::string_view text) {
103 std::size_t count{0UL};
104 for (const char character : text) {
105 if (lector::is_leading_byte(character)) {
106 ++count;
107 }
108 }
109 return count;
110}
111
112/// @brief Computes and returns the length of the longest word in a string of text. The length of a
113/// word is measured by its number of UTF-8 code points.
114/// @param[in] text The string of text whose longest word length is to be computed.
115/// @return The length of the longest word in the string of text.
116[[nodiscard]] inline std::size_t longest_word_length(const std::string_view text) {
117 std::size_t current_longest_word_length{0UL};
118 std::size_t index{0UL};
119 while (index < text.length()) {
120 // Skip over any whitespaces.
121 while (index < text.length() && std::isspace(static_cast<unsigned char>(text[index])) != 0) {
122 ++index;
123 }
124 // Return if the end of the string has been reached after skipping whitespaces.
125 if (index >= text.length()) {
126 break;
127 }
128 // The index now points to the start of the current word.
129 const std::size_t current_word_start{index};
130 // Find the end of the current word.
131 while (index < text.length() && std::isspace(static_cast<unsigned char>(text[index])) == 0) {
132 ++index;
133 }
134 // Obtain the current word.
135 const std::string_view current_word{
136 text.substr(current_word_start, index - current_word_start)};
137 // Compute the length of the current word.
138 const std::size_t current_word_length{lector::code_points(current_word)};
139 // Update the longest word length.
140 current_longest_word_length = std::max(current_longest_word_length, current_word_length);
141 }
142 return current_longest_word_length;
143}
144
145/// @brief Checks whether a string of text contains any whitespace characters.
146/// @param[in] text The string of text to examine.
147/// @return true if the string of text contains any whitespace, or false otherwise.
148[[nodiscard]] inline bool contains_whitespace(const std::string_view text) {
149 return text.find_first_of(" \t\n\v\f\r") != std::string_view::npos;
150}
151
152/// @brief Encloses a string of text in quotes. Either single or double quotes are used depending on
153/// which type of quote is not present in the string of text, with double quotes preferred if
154/// neither type of quote is present. If the string of text already begins and ends with either
155/// single or double quotes, no additional quotes are added. If the string of text is empty, an
156/// empty string is returned.
157/// @param[in] text The string of text to enclose in quotes.
158/// @return The string of text enclosed in quotes.
159/// @throws std::invalid_argument if the string of text contains both single and double quotes.
160[[nodiscard]] inline std::string quote(const std::string_view text) {
161 if (text.empty()) {
162 return std::string{""};
163 }
164 if (text.size() >= static_cast<std::size_t>(2UL) && (text.front() == '"' || text.front() == '\'')
165 && text.front() == text.back()) {
166 return std::string{text};
167 }
168 bool contains_single_quotes{false};
169 bool contains_double_quotes{false};
170 for (const char character : text) {
171 if (character == '\'') {
172 contains_single_quotes = true;
173 } else if (character == '"') {
174 contains_double_quotes = true;
175 }
176 if (contains_single_quotes && contains_double_quotes) {
177 throw std::invalid_argument(
178 "String contains both single and double quotes: " + std::string{text});
179 }
180 }
181 const char quote_character{contains_double_quotes ? '\'' : '"'};
182 std::string result;
183 result.reserve(text.size() + 2);
184 result.push_back(quote_character);
185 result.append(text);
186 result.push_back(quote_character);
187 return result;
188}
189
190/// @brief Encloses a string of text in quotes if it contains any whitespace. Either single or
191/// double quotes are used depending on which type of quote is not present in the string of text,
192/// with double quotes preferred if neither type of quote is present. If the string of text already
193/// begins and ends with either single or double quotes, no additional quotes are added. If the
194/// string of text is empty, an empty string is returned.
195/// @param[in] text The string of text to possibly enclose in quotes.
196/// @return The string of text possibly enclosed in quotes.
197/// @throws std::invalid_argument if the string of text contains both single and double quotes.
198[[nodiscard]] inline std::string quote_if_contains_whitespace(const std::string_view text) {
199 if (lector::contains_whitespace(text)) {
200 return lector::quote(text);
201 }
202 return std::string(text);
203}
204
205/// @brief Tokenizes a string of text into a vector of strings of text, where each string in the
206/// vector corresponds to a word in the original string. Words are defined as sequences of
207/// non-whitespace characters, and whitespace characters are used as delimiters. The function does
208/// not modify the original string and returns views into it, so the original string must remain
209/// valid for the lifetime of the returned vector.
210/// @param[in] text The string of text to be tokenized.
211/// @return A vector of strings of text, each corresponding to a word in the original string.
212[[nodiscard]] inline std::vector<std::string_view> tokenize(const std::string_view text) {
213 std::vector<std::string_view> words;
214 std::size_t begin_index{0UL};
215 while (begin_index < text.size()) {
216 while (begin_index < text.size()
217 && std::isspace(static_cast<unsigned char>(text[begin_index])) != 0) {
218 ++begin_index;
219 }
220 if (begin_index == text.size()) {
221 break;
222 }
223 std::size_t end_index{begin_index};
224 while (
225 end_index < text.size() && std::isspace(static_cast<unsigned char>(text[end_index])) == 0) {
226 ++end_index;
227 }
228 words.push_back(text.substr(begin_index, end_index - begin_index));
229 begin_index = end_index;
230 }
231 return words;
232}
233
234/// @brief Pads a string of text from the left with spaces to reach a specified length. If the
235/// string of text is longer than the specified length, it is unchanged.
236/// @param[in] text The string of text to pad from the left.
237/// @param[in] length The desired length of the padded string.
238/// @return The padded string of text.
239[[nodiscard]] inline std::string pad_left(const std::string_view text, const std::size_t length) {
240 const std::size_t text_length{lector::code_points(text)};
241 if (text_length >= length) {
242 return std::string(text);
243 }
244 return std::string(length - text_length, ' ') + std::string{text};
245}
246
247/// @brief Pads a string of text from the right with spaces to reach a specified length. If the
248/// string of text is longer than the specified length, it is unchanged.
249/// @param[in] text The string of text to pad from the right.
250/// @param[in] length The desired length of the padded string.
251/// @return The padded string of text.
252[[nodiscard]] inline std::string pad_right(const std::string_view text, const std::size_t length) {
253 const std::size_t text_length{lector::code_points(text)};
254 if (text_length >= length) {
255 return std::string(text);
256 }
257 return std::string{text} + std::string(length - text_length, ' ');
258}
259
260/// @brief Joins a vector of strings where each string corresponds to a line of text into a single
261/// string of text, with newline characters inserted between the lines, and the lines left-aligned.
262/// @param[in] lines Vector of strings to be joined and left-aligned.
263/// @return The joined and left-aligned string of text.
264[[nodiscard]] inline std::string join_and_left_align(const std::vector<std::string>& lines) {
265 // Handle the empty case immediately to prevent underflow later.
266 if (lines.empty()) {
267 return std::string{};
268 }
269 // Calculate the exact total size.
270 std::size_t total_size{0UL};
271 for (const std::string& line : lines) {
272 total_size += line.size();
273 }
274 // Add space for the newline separators (one less than the total number of lines).
275 total_size += lines.size() - static_cast<std::size_t>(1UL);
276 // Create and allocate the resulting text.
277 std::string text;
278 text.reserve(total_size);
279 // Append the first line.
280 text.append(lines.front());
281 // Append subsequent lines prefixed by a newline.
282 for (std::size_t line_index{1UL}; line_index < lines.size(); ++line_index) {
283 text.push_back('\n');
284 text.append(lines.at(line_index));
285 }
286 return text;
287}
288
289/// @brief Joins a vector of strings where each string corresponds to a line of text into a single
290/// string of text, with newline characters inserted between the lines, and the lines right-aligned.
291/// @param[in] lines Vector of strings to be joined and right-aligned.
292/// @return The joined and right-aligned string of text.
293[[nodiscard]] inline std::string join_and_right_align(const std::vector<std::string>& lines) {
294 // Handle the empty case immediately to prevent underflow later.
295 if (lines.empty()) {
296 return std::string{};
297 }
298 // Compute the line lengths and find the maximum line length.
299 std::vector<std::size_t> line_lengths;
300 line_lengths.reserve(lines.size());
301 std::size_t longest_line_length{0UL};
302 for (const std::string& line : lines) {
303 const std::size_t length{lector::code_points(line)};
304 line_lengths.push_back(length);
305 longest_line_length = std::max(length, longest_line_length);
306 }
307 // Compute the exact total byte size.
308 std::size_t total_size{0UL};
309 for (std::size_t line_index{0UL}; line_index < lines.size(); ++line_index) {
310 const std::size_t padding{longest_line_length - line_lengths.at(line_index)};
311 total_size += lines.at(line_index).size() + padding;
312 }
313 total_size += lines.size() - static_cast<std::size_t>(1UL);
314 // Create and allocate the resulting text.
315 std::string text;
316 text.reserve(total_size);
317 // Append lines with padding.
318 for (std::size_t line_index{0UL}; line_index < lines.size(); ++line_index) {
319 if (line_index > 0UL) {
320 text.push_back('\n');
321 }
322 const std::size_t padding{longest_line_length - line_lengths.at(line_index)};
323 text.append(padding, ' ');
324 text.append(lines.at(line_index));
325 }
326 return text;
327}
328
329/// @brief Joins a vector of strings where each string corresponds to a line of text into a single
330/// string of text, with newline characters inserted between the lines, and the lines
331/// centre-aligned. If the total required centre-aligning padding is odd, the text is biased by one
332/// space towards the left.
333/// @param[in] lines Vector of strings to be joined and centre-aligned.
334/// @return The joined and centre-aligned string of text.
335[[nodiscard]] inline std::string join_and_centre_align_with_left_bias(
336 const std::vector<std::string>& lines) {
337 // Handle the empty case immediately to prevent underflow later.
338 if (lines.empty()) {
339 return std::string{};
340 }
341 // Compute the line lengths and find the maximum line length.
342 std::vector<std::size_t> line_lengths;
343 line_lengths.reserve(lines.size());
344 std::size_t longest_line_length{0UL};
345 for (const std::string& line : lines) {
346 const std::size_t length{lector::code_points(line)};
347 line_lengths.push_back(length);
348 longest_line_length = std::max(length, longest_line_length);
349 }
350 // Compute the exact total byte size.
351 std::size_t total_size{0UL};
352 for (std::size_t line_index{0UL}; line_index < lines.size(); ++line_index) {
353 const std::size_t total_padding{longest_line_length - line_lengths.at(line_index)};
354 // Bias left. When the total number of padding spaces is odd, integer division rounds down,
355 // giving one less padding space to the left.
356 const std::size_t left_padding{total_padding / 2UL};
357 total_size += lines.at(line_index).size() + left_padding;
358 }
359 total_size += lines.size() - static_cast<std::size_t>(1UL);
360 // Create and allocate the resulting text.
361 std::string text;
362 text.reserve(total_size);
363 // Append lines with padding.
364 for (std::size_t line_index{0UL}; line_index < lines.size(); ++line_index) {
365 if (line_index > 0UL) {
366 text.push_back('\n');
367 }
368 const std::size_t total_padding{longest_line_length - line_lengths.at(line_index)};
369 const std::size_t left_padding{total_padding / 2UL};
370 text.append(left_padding, ' ');
371 text.append(lines.at(line_index));
372 }
373 return text;
374}
375
376/// @brief Joins a vector of strings where each string corresponds to a line of text into a single
377/// string of text, with newline characters inserted between the lines, and the lines
378/// centre-aligned. If the total required centre-aligning padding is odd, the text is biased by one
379/// space towards the right.
380/// @param[in] lines Vector of strings to be joined and centre-aligned.
381/// @return The joined and centre-aligned string of text.
382[[nodiscard]] inline std::string join_and_centre_align_with_right_bias(
383 const std::vector<std::string>& lines) {
384 // Handle the empty case immediately to prevent underflow later.
385 if (lines.empty()) {
386 return std::string{};
387 }
388 // Compute the line lengths and find the maximum line length.
389 std::vector<std::size_t> line_lengths;
390 line_lengths.reserve(lines.size());
391 std::size_t longest_line_length{0UL};
392 for (const std::string& line : lines) {
393 const std::size_t length{lector::code_points(line)};
394 line_lengths.push_back(length);
395 longest_line_length = std::max(length, longest_line_length);
396 }
397 // Compute the exact total byte size.
398 std::size_t total_size{0UL};
399 for (std::size_t line_index{0UL}; line_index < lines.size(); ++line_index) {
400 const std::size_t total_padding{longest_line_length - line_lengths.at(line_index)};
401 // Bias right. When the total number of padding spaces is odd, adding one more space before
402 // performing the integer division rounds it up, giving one more padding space to the left.
403 const std::size_t left_padding{(total_padding + 1UL) / 2UL};
404 total_size += lines.at(line_index).size() + left_padding;
405 }
406 total_size += lines.size() - static_cast<std::size_t>(1UL);
407 // Create and allocate the resulting text.
408 std::string text;
409 text.reserve(total_size);
410 // Append lines with padding.
411 for (std::size_t line_index{0UL}; line_index < lines.size(); ++line_index) {
412 if (line_index > 0UL) {
413 text.push_back('\n');
414 }
415 const std::size_t total_padding{longest_line_length - line_lengths.at(line_index)};
416 const std::size_t left_padding{(total_padding + 1UL) / 2UL};
417 text.append(left_padding, ' ');
418 text.append(lines.at(line_index));
419 }
420 return text;
421}
422
423/// @brief Wraps a string of text to a line length and returns the result as a sequence of strings
424/// of text where each string in the sequence represents one line of text.
425/// @param[in] text The string of text to wrap.
426/// @param[in] line_length The desired line length to use when wrapping. Must be strictly greater
427/// than zero. Very long words whose lengths exceed this line length are hyphenated.
428/// @return The sequence of strings of text that contains one string per line.
429/// @throws std::invalid_argument if the desired line length is zero.
430[[nodiscard]] inline std::vector<std::string> wrap(
431 const std::string_view text, const std::size_t line_length) {
432 // Ensure the line length is valid.
433 if (line_length <= static_cast<std::size_t>(0UL)) {
434 throw std::invalid_argument("Invalid line length. Must be strictly greater than zero.");
435 }
436 // Tokenize the input string of text.
437 const std::vector<std::string_view> words{lector::tokenize(text)};
438 // Process the tokenized input string of text and assemble the wrapped lines.
439 std::vector<std::string> lines;
440 std::string current_line;
441 std::size_t current_line_code_point_size{0UL};
442 for (const std::string_view current_word : words) {
443 // Measure the current word.
444 const std::size_t current_word_code_point_size{lector::code_points(current_word)};
445 const std::size_t space_needed_for_hyphen{
446 (current_line_code_point_size > static_cast<std::size_t>(0UL)) ?
447 static_cast<std::size_t>(1UL) :
448 static_cast<std::size_t>(0UL)};
449 // Check if the current word fits on the current line.
450 if (current_line_code_point_size > static_cast<std::size_t>(0UL)
451 && current_line_code_point_size + current_word_code_point_size + space_needed_for_hyphen
452 <= line_length) {
453 // In this case, the current word fits on the current line.
454 current_line.push_back(' ');
455 current_line.append(current_word);
456 current_line_code_point_size += current_word_code_point_size + space_needed_for_hyphen;
457 } else {
458 // In this case, the current word does not fit on the current line and must be wrapped to the
459 // next line.
460 if (current_line_code_point_size > static_cast<std::size_t>(0UL)) {
461 lines.push_back(std::move(current_line));
462 current_line.clear();
463 current_line_code_point_size = static_cast<std::size_t>(0UL);
464 }
465 // Check if the current word needs to be hyphenated.
466 if (current_word_code_point_size <= line_length) {
467 // In this case, the current word fits completely on an empty line and does not need to be
468 // hyphenated.
469 current_line = current_word;
470 current_line_code_point_size = current_word_code_point_size;
471 } else {
472 // In this case, the current word is too long and must be hyphenated.
473 std::string_view remaining_word{current_word};
474 std::size_t remaining_code_point_size{current_word_code_point_size};
475 // Iterate until the remaining portion of the current word fits on a line, and repeat as
476 // necessary; a very long word might need to be hyphenated multiple times.
477 while (remaining_code_point_size > line_length) {
478 // If the line_length is 1, no hyphen is used. Otherwise, take "line length - 1" code
479 // points to save 1 character for the hyphen.
480 const std::size_t chunk_code_point_size{line_length == static_cast<std::size_t>(1UL) ?
481 static_cast<std::size_t>(1UL) :
482 line_length - static_cast<std::size_t>(1UL)};
483 const std::size_t split_byte_index{
484 lector::byte_interval(remaining_word, chunk_code_point_size).first};
485 std::string split_line(
486 remaining_word.substr(static_cast<std::size_t>(0UL), split_byte_index));
487 if (line_length > static_cast<std::size_t>(1UL)) {
488 split_line.push_back('-');
489 }
490 lines.push_back(std::move(split_line));
491 remaining_word = remaining_word.substr(split_byte_index);
492 remaining_code_point_size -= chunk_code_point_size;
493 }
494 // The remaining slice of the word seeds the subsequent line.
495 if (remaining_code_point_size > static_cast<std::size_t>(0UL)) {
496 current_line = remaining_word;
497 current_line_code_point_size = remaining_code_point_size;
498 }
499 }
500 }
501 }
502 // Push the final built line if it is not empty.
503 if (current_line_code_point_size > static_cast<std::size_t>(0UL)) {
504 lines.push_back(std::move(current_line));
505 }
506 // Return the wrapped lines.
507 return lines;
508}
509
510/// @brief Left-aligns and wraps a string of text to a line length.
511/// @param[in] text The string of text to wrap and left-align.
512/// @param[in] line_length The desired line length to use when wrapping. Must be strictly greater
513/// than zero. Very long words whose lengths exceed this line length are hyphenated.
514/// @return The wrapped and left-aligned string of text.
515/// @throws std::invalid_argument if the desired line length is zero.
516[[nodiscard]] inline std::string wrap_and_left_align(
517 const std::string_view text, const std::size_t line_length) {
518 return lector::join_and_left_align(lector::wrap(text, line_length));
519}
520
521/// @brief Right-aligns and wraps a string of text to a line length.
522/// @param[in] text The string of text to wrap and right-align.
523/// @param[in] line_length The desired line length to use when wrapping. Must be strictly greater
524/// than zero. Very long words whose lengths exceed this line length are hyphenated.
525/// @return The wrapped and right-aligned string of text.
526/// @throws std::invalid_argument if the desired line length is zero.
527[[nodiscard]] inline std::string wrap_and_right_align(
528 const std::string_view text, const std::size_t line_length) {
529 return lector::join_and_right_align(lector::wrap(text, line_length));
530}
531
532/// @brief Centre-aligns and wraps a string of text to a line length. If the total required
533/// centre-aligning padding is odd, the text is biased by one space towards the left.
534/// @param[in] text The string of text to wrap and centre-align.
535/// @param[in] line_length The desired line length to use when wrapping. Must be strictly greater
536/// than zero. Very long words whose lengths exceed this line length are hyphenated.
537/// @return The wrapped and centre-aligned string of text.
538/// @throws std::invalid_argument if the desired line length is zero.
539[[nodiscard]] inline std::string wrap_and_centre_align_with_left_bias(
540 const std::string_view text, const std::size_t line_length) {
542}
543
544/// @brief Centre-aligns and wraps a string of text to a line length. If the total required
545/// centre-aligning padding is odd, the text is biased by one space towards the right.
546/// @param[in] text The string of text to wrap and centre-align.
547/// @param[in] line_length The desired line length to use when wrapping. Must be strictly greater
548/// than zero. Very long words whose lengths exceed this line length are hyphenated.
549/// @return The wrapped and centre-aligned string of text.
550/// @throws std::invalid_argument if the desired line length is zero.
551[[nodiscard]] inline std::string wrap_and_centre_align_with_right_bias(
552 const std::string_view text, const std::size_t line_length) {
554}
555
556/// @brief Collates two strings of text, each representing a column, into a single string that
557/// contains newline-separated lines of text, with the lines formatted such that the two columns are
558/// left-aligned and spaced a short distance apart.
559/// @param[in] first_column_text The string of text for the first column.
560/// @param[in] first_column_width The desired width of the first column. Very long words whose
561/// length exceeds this width are hyphenated.
562/// @param[in] second_column_text The string of text for the second column.
563/// @param[in] second_column_width The desired width of the second column. Very long words whose
564/// length exceeds this width are hyphenated.
565/// @return The string that contains the collated text.
566/// @throws std::invalid_argument if either desired column width is zero.
567[[nodiscard]] inline std::string collate_and_left_align(
568 const std::string_view first_column_text, const std::size_t first_column_width,
569 const std::string_view second_column_text, const std::size_t second_column_width) {
570 // Use a gutter width of two spaces.
571 constexpr std::size_t gutter_width{2UL};
572 // Wrap and split both columns.
573 const std::vector<std::string> first_column{lector::wrap(first_column_text, first_column_width)};
574 const std::vector<std::string> second_column{
575 lector::wrap(second_column_text, second_column_width)};
576 // Determine the total number of rows required.
577 const std::size_t rows{std::max(first_column.size(), second_column.size())};
578 // Pre-allocate memory for the result. A safe and highly efficient upper bound is the byte size of
579 // both original input strings, plus the maximum possible padding spaces and newlines per row.
580 std::string result;
581 result.reserve(first_column_text.length() + second_column_text.length()
582 + (rows * (first_column_width + gutter_width + static_cast<std::size_t>(1UL))));
583 // Collate the rows line by line.
584 for (std::size_t row_index{0UL}; row_index < rows; ++row_index) {
585 // Append a newline character for every row after the first to separate them without leaving a
586 // trailing newline at the very end of the string.
587 if (row_index > static_cast<std::size_t>(0UL)) {
588 result.push_back('\n');
589 }
590 // Grab the string for the first column if it exists on this row; otherwise, use an empty
591 // string.
592 const std::string_view first_cell{
593 row_index < first_column.size() ? std::string_view{first_column.at(row_index)} :
594 std::string_view{}};
595 result.append(first_cell);
596 // If the second column has text on this row, pad the first column and append the second column.
597 // Otherwise, if the second column is exhausted, skip this padding to avoid unnecessary trailing
598 // whitespace.
599 if (row_index < second_column.size()) {
600 const std::size_t first_cell_length{lector::code_points(first_cell)};
601 const std::size_t padding{first_column_width + gutter_width - first_cell_length};
602 result.append(padding, ' ');
603 result.append(second_column.at(row_index));
604 }
605 }
606 return result;
607}
608
609/// @brief Collates two strings of text, each representing a column, into a single string that
610/// contains newline-separated lines of text, with the lines formatted such that the two columns are
611/// right-aligned and spaced a short distance apart.
612/// @param[in] first_column_text The string of text for the first column.
613/// @param[in] first_column_width The desired width of the first column. Very long words whose
614/// length exceeds this width are hyphenated.
615/// @param[in] second_column_text The string of text for the second column.
616/// @param[in] second_column_width The desired width of the second column. Very long words whose
617/// length exceeds this width are hyphenated.
618/// @return The string that contains the collated text.
619/// @throws std::invalid_argument if either desired column width is zero.
620[[nodiscard]] inline std::string collate_and_right_align(
621 const std::string_view first_column_text, const std::size_t first_column_width,
622 const std::string_view second_column_text, const std::size_t second_column_width) {
623 // Use a gutter width of two spaces.
624 constexpr std::size_t gutter_width{2UL};
625 // Wrap and split both columns.
626 const std::vector<std::string> first_column{lector::wrap(first_column_text, first_column_width)};
627 const std::vector<std::string> second_column{
628 lector::wrap(second_column_text, second_column_width)};
629 // Determine the total number of rows required.
630 const std::size_t rows{std::max(first_column.size(), second_column.size())};
631 // Pre-allocate memory for the result. A safe and highly efficient upper bound is the byte size of
632 // both original input strings, plus the maximum possible padding spaces and newlines per row.
633 std::string result;
634 result.reserve(first_column_text.length() + second_column_text.length()
635 + (rows
636 * (first_column_width + gutter_width + second_column_width
637 + static_cast<std::size_t>(1UL))));
638 // Collate the rows line by line.
639 for (std::size_t row_index{0UL}; row_index < rows; ++row_index) {
640 // Append a newline character for every row after the first to separate them without leaving a
641 // trailing newline at the very end of the string.
642 if (row_index > static_cast<std::size_t>(0UL)) {
643 result.push_back('\n');
644 }
645 // Grab the string for the first column if it exists on this row; otherwise, use an empty
646 // string.
647 const std::string_view first_cell{
648 row_index < first_column.size() ? std::string_view{first_column.at(row_index)} :
649 std::string_view{}};
650 const std::size_t first_cell_length{lector::code_points(first_cell)};
651 // Calculate the leading padding. The ternary operator protects against std::size_t underflow in
652 // the extremely unlikely event a cell exceeds the column width.
653 const std::size_t first_cell_padding{
654 first_column_width > first_cell_length ? first_column_width - first_cell_length :
655 static_cast<std::size_t>(0UL)};
656 // Right-align the first column by prepending the required padding.
657 result.append(first_cell_padding, ' ');
658 result.append(first_cell);
659 // If the second column has text on this row, append the gutter, pad the second column, and
660 // append it. Otherwise, if the second column is exhausted, skip this padding to avoid
661 // unnecessary trailing whitespace.
662 if (row_index < second_column.size()) {
663 const std::string_view second_cell{second_column.at(row_index)};
664 const std::size_t second_cell_length{lector::code_points(second_cell)};
665 const std::size_t second_cell_padding{
666 second_column_width > second_cell_length ? second_column_width - second_cell_length :
667 static_cast<std::size_t>(0UL)};
668 result.append(gutter_width, ' ');
669 result.append(second_cell_padding, ' ');
670 result.append(second_cell);
671 }
672 }
673 return result;
674}
675
676/// @brief Collates two strings of text, each representing a column, into a single string that
677/// contains newline-separated lines of text, with the lines formatted such that the two columns are
678/// centre-aligned and spaced a short distance apart. If the total required centre-aligning padding
679/// is odd, the text is biased by one space towards the left.
680/// @param[in] first_column_text The string of text for the first column.
681/// @param[in] first_column_width The desired width of the first column. Very long words whose
682/// length exceeds this width are hyphenated.
683/// @param[in] second_column_text The string of text for the second column.
684/// @param[in] second_column_width The desired width of the second column. Very long words whose
685/// length exceeds this width are hyphenated.
686/// @return The string that contains the collated text.
687/// @throws std::invalid_argument if either desired column width is zero.
688[[nodiscard]] inline std::string collate_and_centre_align_with_left_bias(
689 const std::string_view first_column_text, const std::size_t first_column_width,
690 const std::string_view second_column_text, const std::size_t second_column_width) {
691 // Use a gutter width of two spaces.
692 constexpr std::size_t gutter_width{2UL};
693 // Wrap and split both columns.
694 const std::vector<std::string> first_column{lector::wrap(first_column_text, first_column_width)};
695 const std::vector<std::string> second_column{
696 lector::wrap(second_column_text, second_column_width)};
697 // Determine the total number of rows required.
698 const std::size_t rows{std::max(first_column.size(), second_column.size())};
699 // Pre-allocate memory for the result. A safe and highly efficient upper bound is the byte size of
700 // both original input strings, plus the maximum possible padding spaces and newlines per row.
701 std::string result;
702 result.reserve(first_column_text.length() + second_column_text.length()
703 + (rows
704 * (first_column_width + gutter_width + second_column_width
705 + static_cast<std::size_t>(1UL))));
706 // Collate the rows line by line.
707 for (std::size_t row_index{0UL}; row_index < rows; ++row_index) {
708 // Append a newline character for every row after the first to separate them without leaving a
709 // trailing newline at the very end of the string.
710 if (row_index > static_cast<std::size_t>(0UL)) {
711 result.push_back('\n');
712 }
713 // Grab the string for the first column if it exists on this row; otherwise, use an empty
714 // string.
715 const std::string_view first_cell{
716 row_index < first_column.size() ? std::string_view{first_column.at(row_index)} :
717 std::string_view{}};
718 const std::size_t first_cell_length{lector::code_points(first_cell)};
719 // Calculate the total padding. The ternary operator protects against std::size_t underflow in
720 // the extremely unlikely event a cell exceeds the column width.
721 const std::size_t first_cell_total_padding{
722 first_column_width > first_cell_length ? first_column_width - first_cell_length :
723 static_cast<std::size_t>(0UL)};
724 // Bias left. When the total number of padding spaces is odd, integer division rounds down,
725 // giving one less padding space to the left.
726 const std::size_t first_cell_left_padding{first_cell_total_padding / 2UL};
727 const std::size_t first_cell_right_padding{first_cell_total_padding - first_cell_left_padding};
728 // Append the left padding and the first cell.
729 result.append(first_cell_left_padding, ' ');
730 result.append(first_cell);
731 // If the second column has text on this row, calculate its padding, append the central padding
732 // (first cell right padding + gutter + second cell left padding), and append the second cell.
733 if (row_index < second_column.size()) {
734 const std::string_view second_cell{second_column.at(row_index)};
735 const std::size_t second_cell_length{lector::code_points(second_cell)};
736 const std::size_t second_cell_total_padding{
737 second_column_width > second_cell_length ? second_column_width - second_cell_length :
738 static_cast<std::size_t>(0UL)};
739 const std::size_t second_cell_left_padding{second_cell_total_padding / 2UL};
740 const std::size_t central_padding{
741 first_cell_right_padding + gutter_width + second_cell_left_padding};
742 result.append(central_padding, ' ');
743 result.append(second_cell);
744 }
745 }
746 return result;
747}
748
749/// @brief Collates two strings of text, each representing a column, into a single string that
750/// contains newline-separated lines of text, with the lines formatted such that the two columns are
751/// centre-aligned and spaced a short distance apart. If the total required centre-aligning padding
752/// is odd, the text is biased by one space towards the right.
753/// @param[in] first_column_text The string of text for the first column.
754/// @param[in] first_column_width The desired width of the first column. Very long words whose
755/// length exceeds this width are hyphenated.
756/// @param[in] second_column_text The string of text for the second column.
757/// @param[in] second_column_width The desired width of the second column. Very long words whose
758/// length exceeds this width are hyphenated.
759/// @return The string that contains the collated text.
760/// @throws std::invalid_argument if either desired column width is zero.
761[[nodiscard]] inline std::string collate_and_centre_align_with_right_bias(
762 const std::string_view first_column_text, const std::size_t first_column_width,
763 const std::string_view second_column_text, const std::size_t second_column_width) {
764 // Use a gutter width of two spaces.
765 constexpr std::size_t gutter_width{2UL};
766 // Wrap and split both columns.
767 const std::vector<std::string> first_column{lector::wrap(first_column_text, first_column_width)};
768 const std::vector<std::string> second_column{
769 lector::wrap(second_column_text, second_column_width)};
770 // Determine the total number of rows required.
771 const std::size_t rows{std::max(first_column.size(), second_column.size())};
772 // Pre-allocate memory for the result. A safe and highly efficient upper bound is the byte size of
773 // both original input strings, plus the maximum possible padding spaces and newlines per row.
774 std::string result;
775 result.reserve(first_column_text.length() + second_column_text.length()
776 + (rows
777 * (first_column_width + gutter_width + second_column_width
778 + static_cast<std::size_t>(1UL))));
779 // Collate the rows line by line.
780 for (std::size_t row_index{0UL}; row_index < rows; ++row_index) {
781 // Append a newline character for every row after the first to separate them without leaving a
782 // trailing newline at the very end of the string.
783 if (row_index > static_cast<std::size_t>(0UL)) {
784 result.push_back('\n');
785 }
786 // Grab the string for the first column if it exists on this row; otherwise, use an empty
787 // string.
788 const std::string_view first_cell{
789 row_index < first_column.size() ? std::string_view{first_column.at(row_index)} :
790 std::string_view{}};
791 const std::size_t first_cell_length{lector::code_points(first_cell)};
792 // Calculate the total padding. The ternary operator protects against std::size_t underflow in
793 // the extremely unlikely event a cell exceeds the column width.
794 const std::size_t first_cell_total_padding{
795 first_column_width > first_cell_length ? first_column_width - first_cell_length :
796 static_cast<std::size_t>(0UL)};
797 // Bias right. When the total number of padding spaces is odd, adding one more space before
798 // performing the integer division rounds it up, giving one more padding space to the left.
799 const std::size_t first_cell_left_padding{(first_cell_total_padding + 1UL) / 2UL};
800 const std::size_t first_cell_right_padding{first_cell_total_padding - first_cell_left_padding};
801 // Append the left padding and the first cell.
802 result.append(first_cell_left_padding, ' ');
803 result.append(first_cell);
804 // If the second column has text on this row, calculate its padding, append the central padding
805 // (first cell right padding + gutter + second cell left padding), and append the second cell.
806 if (row_index < second_column.size()) {
807 const std::string_view second_cell{second_column.at(row_index)};
808 const std::size_t second_cell_length{lector::code_points(second_cell)};
809 const std::size_t second_cell_total_padding{
810 second_column_width > second_cell_length ? second_column_width - second_cell_length :
811 static_cast<std::size_t>(0UL)};
812 const std::size_t second_cell_left_padding{(second_cell_total_padding + 1UL) / 2UL};
813 const std::size_t central_padding{
814 first_cell_right_padding + gutter_width + second_cell_left_padding};
815 result.append(central_padding, ' ');
816 result.append(second_cell);
817 }
818 }
819 return result;
820}
821
822} // namespace lector
823
824#endif // LECTOR_TEXT_HPP
The Lector library's namespace.
Definition arguments.hpp:43
std::string wrap_and_right_align(const std::string_view text, const std::size_t line_length)
Right-aligns and wraps a string of text to a line length.
Definition text.hpp:527
std::string quote_if_contains_whitespace(const std::string_view text)
Encloses a string of text in quotes if it contains any whitespace. Either single or double quotes are...
Definition text.hpp:198
bool is_leading_byte(const char character)
Returns whether a given character is the leading byte of a UTF-8 character. All UTF-8 characters meas...
Definition text.hpp:56
std::string collate_and_centre_align_with_left_bias(const std::string_view first_column_text, const std::size_t first_column_width, const std::string_view second_column_text, const std::size_t second_column_width)
Collates two strings of text, each representing a column, into a single string that contains newline-...
Definition text.hpp:688
std::string wrap_and_left_align(const std::string_view text, const std::size_t line_length)
Left-aligns and wraps a string of text to a line length.
Definition text.hpp:516
std::string collate_and_centre_align_with_right_bias(const std::string_view first_column_text, const std::size_t first_column_width, const std::string_view second_column_text, const std::size_t second_column_width)
Collates two strings of text, each representing a column, into a single string that contains newline-...
Definition text.hpp:761
std::string join_and_centre_align_with_right_bias(const std::vector< std::string > &lines)
Joins a vector of strings where each string corresponds to a line of text into a single string of tex...
Definition text.hpp:382
std::size_t code_points(const std::string_view text)
Counts and returns the number of UTF-8 code points in a string of text. The number of UTF-8 code poin...
Definition text.hpp:102
std::size_t longest_word_length(const std::string_view text)
Computes and returns the length of the longest word in a string of text. The length of a word is meas...
Definition text.hpp:116
std::string collate_and_right_align(const std::string_view first_column_text, const std::size_t first_column_width, const std::string_view second_column_text, const std::size_t second_column_width)
Collates two strings of text, each representing a column, into a single string that contains newline-...
Definition text.hpp:620
std::pair< std::size_t, std::size_t > byte_interval(const std::string_view text, const std::size_t code_point_index)
Finds the exact byte [begin, end) index interval in a string of text where a specified code point res...
Definition text.hpp:72
std::string wrap_and_centre_align_with_left_bias(const std::string_view text, const std::size_t line_length)
Centre-aligns and wraps a string of text to a line length. If the total required centre-aligning padd...
Definition text.hpp:539
std::string collate_and_left_align(const std::string_view first_column_text, const std::size_t first_column_width, const std::string_view second_column_text, const std::size_t second_column_width)
Collates two strings of text, each representing a column, into a single string that contains newline-...
Definition text.hpp:567
std::string join_and_left_align(const std::vector< std::string > &lines)
Joins a vector of strings where each string corresponds to a line of text into a single string of tex...
Definition text.hpp:264
std::vector< std::string_view > tokenize(const std::string_view text)
Tokenizes a string of text into a vector of strings of text, where each string in the vector correspo...
Definition text.hpp:212
std::string join_and_right_align(const std::vector< std::string > &lines)
Joins a vector of strings where each string corresponds to a line of text into a single string of tex...
Definition text.hpp:293
std::string pad_right(const std::string_view text, const std::size_t length)
Pads a string of text from the right with spaces to reach a specified length. If the string of text i...
Definition text.hpp:252
std::string join_and_centre_align_with_left_bias(const std::vector< std::string > &lines)
Joins a vector of strings where each string corresponds to a line of text into a single string of tex...
Definition text.hpp:335
std::string quote(const std::string_view text)
Encloses a string of text in quotes. Either single or double quotes are used depending on which type ...
Definition text.hpp:160
std::vector< std::string > wrap(const std::string_view text, const std::size_t line_length)
Wraps a string of text to a line length and returns the result as a sequence of strings of text where...
Definition text.hpp:430
bool contains_whitespace(const std::string_view text)
Checks whether a string of text contains any whitespace characters.
Definition text.hpp:148
std::string pad_left(const std::string_view text, const std::size_t length)
Pads a string of text from the left with spaces to reach a specified length. If the string of text is...
Definition text.hpp:239
std::string wrap_and_centre_align_with_right_bias(const std::string_view text, const std::size_t line_length)
Centre-aligns and wraps a string of text to a line length. If the total required centre-aligning padd...
Definition text.hpp:551