|
1 | 1 | //////////////////////////////////////////////////////////////////////////////// |
2 | 2 | // |
3 | | -// Copyright 2016 - 2021, 2023, Gothenburg Bit Factory. |
| 3 | +// Copyright 2016 - 2021, 2023, 2026 Gothenburg Bit Factory. |
4 | 4 | // |
5 | 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy |
6 | 6 | // of this software and associated documentation files (the "Software"), to deal |
|
25 | 25 | //////////////////////////////////////////////////////////////////////////////// |
26 | 26 |
|
27 | 27 | #include <Composite.h> |
| 28 | +#include <format.h> |
| 29 | +#include <limits> |
28 | 30 | #include <sstream> |
29 | | -#include <stack> |
30 | 31 | #include <utf8.h> |
31 | 32 |
|
| 33 | + |
| 34 | +//////////////////////////////////////////////////////////////////////////////// |
| 35 | + |
| 36 | +namespace |
| 37 | +{ |
| 38 | + |
| 39 | + // Helper function that either replaces a pre-existing element at index (i) in |
| 40 | + // a std::vector with the value (x) (if (i) is less than the size of the vector) |
| 41 | + // or extends the vector in such a way that it ends up with (i+1) elements, with |
| 42 | + // the value (x) at index (i) and the padding value (pad) at each index between |
| 43 | + // that of the final pre-existing element of the vector and (i). |
| 44 | + template <typename T> |
| 45 | + void put_or_extend ( |
| 46 | + std::vector<T>& v, typename std::vector<T>::size_type i, const T& x, const T& pad = T {}) |
| 47 | + { |
| 48 | + if (i < v.size ()) |
| 49 | + v[i] = x; |
| 50 | + else |
| 51 | + { |
| 52 | + v.resize (i, pad); |
| 53 | + v.push_back (x); |
| 54 | + } |
| 55 | + } |
| 56 | + |
| 57 | + // Helper class that is used to store information about columns in a Composite. |
| 58 | + struct ColumnData |
| 59 | + { |
| 60 | + // Number of topmost layer that overlaps with the column represented by this ColumnData. |
| 61 | + // NOTE: Layer numbers start at 1. "Layer 0" is background not covered by any layer. |
| 62 | + unsigned int layer_num; |
| 63 | + |
| 64 | + // Byte offset into the UTF-8 text string of the layer identified by (layer_num). |
| 65 | + // Points to the first byte of the first character to include in the content |
| 66 | + // of the column represented by this ColumnData. |
| 67 | + std::string::size_type text_begin_i; |
| 68 | + |
| 69 | + // Byte offset into the UTF-8 text string of the layer identified by (layer_num). |
| 70 | + // Points to the first byte after the last character to include in the content |
| 71 | + // of the column represented by this ColumnData. |
| 72 | + std::string::size_type text_end_i; |
| 73 | + |
| 74 | + // Unicode display width of the first character to include in the content |
| 75 | + // of the column represented by this ColumnData. Should always be 1 or 2, |
| 76 | + // unless this ColumnData represents a padding column. |
| 77 | + unsigned char char_0_width; |
| 78 | + |
| 79 | + ColumnData ( |
| 80 | + unsigned int layer = 0, std::string::size_type begin_i = 1, std::string::size_type end_i = 0, |
| 81 | + unsigned char c_0_w = 0) |
| 82 | + : |
| 83 | + layer_num (layer), text_begin_i (begin_i), text_end_i (end_i), char_0_width (c_0_w) |
| 84 | + {} |
| 85 | + |
| 86 | + ColumnData (const ColumnData& orig) = default; |
| 87 | + |
| 88 | + ColumnData& operator= (const ColumnData& orig) = default; |
| 89 | + |
| 90 | + std::string::difference_type byte_count () const |
| 91 | + { |
| 92 | + return text_end_i - text_begin_i; |
| 93 | + } |
| 94 | + |
| 95 | + // Changes the state of this ColumnData to one that indicates that the ColumnData |
| 96 | + // represents a padding column (i.e. a state where byte_count is negative). |
| 97 | + void make_padding () |
| 98 | + { |
| 99 | + text_begin_i = 1; |
| 100 | + text_end_i = 0; |
| 101 | + char_0_width = 0; |
| 102 | + } |
| 103 | + |
| 104 | + bool is_padding () const |
| 105 | + { |
| 106 | + return byte_count () < 0; |
| 107 | + } |
| 108 | + }; |
| 109 | + |
| 110 | + const ColumnData LAYER_0_PAD; // ColumnData representing a padding column on "layer 0". |
| 111 | + |
| 112 | + // Special column index value, distinct from any valid column index. |
| 113 | + const std::string::size_type INVALID_COLUMN_I = std::numeric_limits<std::string::size_type>::max (); |
| 114 | + |
| 115 | + // Helper function that turns the uncovered half of half-covered wide characters into padding. |
| 116 | + inline void do_halfcovered_wide_char_check ( |
| 117 | + std::vector<ColumnData>& columns, std::vector<ColumnData>::size_type column_i) |
| 118 | + { |
| 119 | + // If there is a wide character (on a lower layer) in the preceding column, replace |
| 120 | + // that character (and any nonspacing characters associated with it) with padding. |
| 121 | + // (Because the second half of that character will be covered, and we couldn't display |
| 122 | + // half a character if we wanted to.) |
| 123 | + if (column_i >= 1 && column_i - 1 < columns.size ()) |
| 124 | + { |
| 125 | + ColumnData& prev_col_data = columns[column_i - 1]; |
| 126 | + if (prev_col_data.char_0_width == 2) |
| 127 | + prev_col_data.make_padding (); |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | +}; |
| 132 | + |
32 | 133 | //////////////////////////////////////////////////////////////////////////////// |
33 | 134 | // Initially assume no text, but infinite virtual space. |
34 | 135 | // |
@@ -74,65 +175,114 @@ void Composite::add ( |
74 | 175 | // bbbbb // Layer 2 |
75 | 176 | // c // Layer 3 |
76 | 177 | // |
77 | | -// Walk all strings left to right, selecting the character and color from the |
| 178 | +// Walk all layers left to right, selecting the character and color from the |
78 | 179 | // highest numbered layer. Emit color codes only on edge detection. |
79 | 180 | // |
80 | 181 | std::string Composite::str () const |
81 | 182 | { |
82 | | - // The strings are broken into a vector of int, for UTF8 support. |
83 | | - std::vector <int> characters; |
84 | | - std::vector <int> colors; |
85 | | - for (unsigned int layer = 0; layer < _layers.size (); ++layer) |
| 183 | + std::vector <ColumnData> columns; |
| 184 | + |
| 185 | + for (unsigned int layer_i = 0; layer_i < _layers.size (); ++layer_i) |
86 | 186 | { |
87 | | - const auto& text = std::get <0> (_layers[layer]); |
88 | | - auto offset = std::get <1> (_layers[layer]); |
89 | | - auto len = utf8_text_length (text); |
| 187 | + const auto& text = std::get <0> (_layers[layer_i]); |
| 188 | + auto offset = std::get <1> (_layers[layer_i]); |
| 189 | + auto len = utf8_text_length (text); |
90 | 190 |
|
91 | | - // Make sure the vectors are large enough to support a write operator[]. |
92 | | - if (characters.size () < offset + len) |
93 | | - { |
94 | | - characters.resize (offset + len, 32); |
95 | | - colors.resize (offset + len, 0); |
96 | | - } |
| 191 | + // Make sure the capacity of the column vector is large enough to support push_back() |
| 192 | + // without reallocation. |
| 193 | + if (columns.capacity () < offset + len) |
| 194 | + columns.reserve (offset + len); |
97 | 195 |
|
98 | | - // Copy in the layer characters and color indexes. |
| 196 | + // Inspect and decide how to handle each character (i.e. Unicode code point) |
| 197 | + // in the current layer's text string. |
| 198 | + std::string::size_type prev_cursor = 0; |
99 | 199 | std::string::size_type cursor = 0; |
100 | | - int character; |
101 | | - int count = 0; |
| 200 | + unsigned int column_count = 0; |
| 201 | + std::string::size_type prev_spacer_column_i = INVALID_COLUMN_I; |
| 202 | + unsigned int character; |
102 | 203 | while ((character = utf8_next_char (text, cursor))) |
103 | 204 | { |
104 | | - characters[offset + count] = character; |
105 | | - colors [offset + count] = layer + 1; |
106 | | - ++count; |
| 205 | + std::string::size_type column_i = offset + column_count; |
| 206 | + int ch_width = mk_wcwidth ((wchar_t)character); |
| 207 | + |
| 208 | + switch (ch_width) |
| 209 | + { |
| 210 | + case 0: // zero-width / nonspacing character |
| 211 | + if (prev_spacer_column_i == INVALID_COLUMN_I) // No preceding spacing character on this layer. |
| 212 | + ; // Skip this character. |
| 213 | + else // There is a preceding spacing character on this layer. |
| 214 | + { |
| 215 | + // Append the nonspacing character to the column of the previous spacing character. |
| 216 | + columns[prev_spacer_column_i].text_end_i = cursor; |
| 217 | + } |
| 218 | + break; |
| 219 | + case 1: // ordinary narrow spacing character |
| 220 | + if (prev_spacer_column_i == INVALID_COLUMN_I) |
| 221 | + do_halfcovered_wide_char_check (columns, column_i); |
| 222 | + |
| 223 | + // Put the character in the appropriate column. Pad out the column list as necessary. |
| 224 | + put_or_extend (columns, column_i, ColumnData (layer_i + 1, prev_cursor, cursor, 1), LAYER_0_PAD); |
| 225 | + |
| 226 | + prev_spacer_column_i = column_i; |
| 227 | + column_count += 1; |
| 228 | + break; |
| 229 | + case 2: // graphically wide spacing character |
| 230 | + if (prev_spacer_column_i == INVALID_COLUMN_I) |
| 231 | + do_halfcovered_wide_char_check (columns, column_i); |
| 232 | + |
| 233 | + // Put the character in the appropriate column. Pad out the column list as necessary. |
| 234 | + // Make the column after the current one (which is also covered by the wide character) |
| 235 | + // a padding column on the current layer. |
| 236 | + put_or_extend (columns, column_i, ColumnData (layer_i + 1, prev_cursor, cursor, 2), LAYER_0_PAD); |
| 237 | + put_or_extend (columns, column_i + 1, ColumnData (layer_i + 1), LAYER_0_PAD); |
| 238 | + |
| 239 | + prev_spacer_column_i = column_i; |
| 240 | + column_count += 2; |
| 241 | + break; |
| 242 | + default: // Should not happen. |
| 243 | + throw format ("Unexpected character width {1} of code point 0x{2}.", ch_width, formatHex (character)); |
| 244 | + } |
| 245 | + |
| 246 | + // Remember byte offset of first UTF-8 byte of next character in the layer text. |
| 247 | + prev_cursor = cursor; |
107 | 248 | } |
108 | 249 | } |
109 | 250 |
|
110 | | - // Now walk the character and color vector, emitting every character and |
111 | | - // every detected color change. |
| 251 | + // Now walk the column vector, emitting every character and every detected layer change. |
112 | 252 | std::stringstream out; |
113 | | - int prev_color = 0; |
114 | | - for (unsigned int i = 0; i < characters.size (); ++i) |
| 253 | + unsigned int prev_layer = 0; |
| 254 | + for (unsigned int column_i = 0; column_i < columns.size (); ++column_i) |
115 | 255 | { |
116 | | - // A change in color triggers a code emit. |
117 | | - if (prev_color != colors[i]) |
| 256 | + auto column_data = columns[column_i]; |
| 257 | + auto curr_layer = column_data.layer_num; |
| 258 | + const auto& text = std::get <0> (_layers[curr_layer - 1]); |
| 259 | + |
| 260 | + // A change in layer triggers an ANSI escape code emit. |
| 261 | + if (prev_layer != curr_layer) |
118 | 262 | { |
119 | | - if (prev_color) |
120 | | - out << std::get <2> (_layers[prev_color - 1]).end (); |
| 263 | + if (prev_layer) // Reset attributes (if any) of previous layer. |
| 264 | + out << std::get <2> (_layers[prev_layer - 1]).end (); |
121 | 265 |
|
122 | | - if (colors[i]) |
123 | | - out << std::get <2> (_layers[colors[i] - 1]).code (); |
124 | | - else |
125 | | - out << std::get <2> (_layers[prev_color - 1]).end (); |
| 266 | + if (curr_layer) // Set attributes (if any) of current layer. |
| 267 | + out << std::get <2> (_layers[curr_layer - 1]).code (); |
126 | 268 |
|
127 | | - prev_color = colors[i]; |
| 269 | + prev_layer = curr_layer; |
128 | 270 | } |
129 | 271 |
|
130 | | - out << utf8_character (characters[i]); |
| 272 | + // The layer text string is already UTF-8, so we can output its bytes verbatim, |
| 273 | + // provided that we're keeping track of character (i.e. code point) boundaries. |
| 274 | + if (column_data.is_padding ()) |
| 275 | + out << ' '; // Display padding columns as spaces. |
| 276 | + else // Display a slice of the layer text (Spacer [Nonspacer ...]). |
| 277 | + out.write(text.data () + column_data.text_begin_i, column_data.byte_count ()); |
| 278 | + |
| 279 | + if (column_data.char_0_width == 2) |
| 280 | + ++column_i; // Wide characters cover two columns. |
131 | 281 | } |
132 | 282 |
|
133 | 283 | // Terminate the color codes, if necessary. |
134 | | - if (prev_color) |
135 | | - out << std::get <2> (_layers[prev_color - 1]).end (); |
| 284 | + if (prev_layer) |
| 285 | + out << std::get <2> (_layers[prev_layer - 1]).end (); |
136 | 286 |
|
137 | 287 | return out.str (); |
138 | 288 | } |
|
0 commit comments