Porytiles
Loading...
Searching...
No Matches
text_wrap.cpp
Go to the documentation of this file.
2
3#include <algorithm>
4#include <cstddef>
5#include <string>
6#include <utility>
7#include <vector>
8
9namespace {
10
11using namespace porytiles;
12
13const std::string ansi_reset = "\033[0m";
14
16struct Cell {
17 std::string prefix;
18 std::string glyph;
19 bool is_space;
20};
21
23std::string consume_escape(const std::string &line, std::size_t &pos)
24{
25 const std::size_t start = pos;
26 pos += 2; // skip the ESC and '['
27 // CSI parameters/intermediates run until a final byte in the 0x40-0x7E range.
28 while (pos < line.size() &&
29 (static_cast<unsigned char>(line[pos]) < 0x40 || static_cast<unsigned char>(line[pos]) > 0x7E)) {
30 ++pos;
31 }
32 if (pos < line.size()) {
33 ++pos; // include the final byte
34 }
35 return line.substr(start, pos - start);
36}
37
39std::string consume_codepoint(const std::string &line, std::size_t &pos)
40{
41 const std::size_t start = pos;
42 const auto lead = static_cast<unsigned char>(line[pos]);
43 std::size_t len = 1;
44 if ((lead & 0x80U) != 0) {
45 if ((lead & 0xE0U) == 0xC0U) {
46 len = 2;
47 }
48 else if ((lead & 0xF0U) == 0xE0U) {
49 len = 3;
50 }
51 else if ((lead & 0xF8U) == 0xF0U) {
52 len = 4;
53 }
54 }
55 len = std::min(len, line.size() - pos);
56 pos += len;
57 return line.substr(start, len);
58}
59
61std::vector<Cell> tokenize(const std::string &line, std::string &trailing_out)
62{
63 std::vector<Cell> cells;
64 std::string pending_prefix;
65 std::size_t pos = 0;
66 while (pos < line.size()) {
67 if (static_cast<unsigned char>(line[pos]) == 0x1B && pos + 1 < line.size() && line[pos + 1] == '[') {
68 pending_prefix += consume_escape(line, pos);
69 }
70 else {
71 std::string glyph = consume_codepoint(line, pos);
72 const bool is_space = glyph == " ";
73 cells.push_back(Cell{std::move(pending_prefix), std::move(glyph), is_space});
74 pending_prefix.clear();
75 }
76 }
77 trailing_out = pending_prefix;
78 return cells;
79}
80
82void apply_sgr(std::string &active, const std::string &prefix)
83{
84 std::size_t pos = 0;
85 while (pos < prefix.size()) {
86 const std::size_t start = pos;
87 std::string seq = consume_escape(prefix, pos);
88 if (seq == ansi_reset || seq == "\033[m") {
89 active.clear();
90 }
91 else {
92 active += seq;
93 }
94 }
95}
96
97} // namespace
98
99namespace porytiles {
100
101std::vector<std::string> wrap_ansi_line(const std::string &line, const std::size_t width)
102{
103 if (width == 0) {
104 return {line};
105 }
106
107 std::string trailing;
108 const std::vector<Cell> cells = tokenize(line, trailing);
109 if (cells.empty()) {
110 return {line};
111 }
112
113 // active_entering[i] is the SGR state in effect just before cell i's own prefix, i.e. the styling a wrapped
114 // continuation line must re-open when it starts at cell i. active_entering[cells.size()] is the state after the
115 // final cell, used to decide whether a line needs a trailing reset.
116 std::vector<std::string> active_entering(cells.size() + 1);
117 for (std::size_t i = 0; i < cells.size(); ++i) {
118 active_entering[i + 1] = active_entering[i];
119 apply_sgr(active_entering[i + 1], cells[i].prefix);
120 }
121
122 // First pass: choose the [start, end) cell ranges for each physical line, breaking at spaces where possible.
123 std::vector<std::pair<std::size_t, std::size_t>> ranges;
124 std::size_t i = 0;
125 while (i < cells.size()) {
126 const std::size_t line_start = i;
127 std::size_t visible = 0;
128 std::size_t last_space = cells.size(); // sentinel: no break opportunity seen yet
129 std::size_t j = i;
130 while (j < cells.size() && visible < width) {
131 if (cells[j].is_space) {
132 last_space = j;
133 }
134 ++visible;
135 ++j;
136 }
137 if (j == cells.size()) {
138 ranges.emplace_back(line_start, cells.size());
139 break;
140 }
141 if (cells[j].is_space) {
142 // The break lands exactly on a space: end the line here and swallow the run of spaces.
143 ranges.emplace_back(line_start, j);
144 while (j < cells.size() && cells[j].is_space) {
145 ++j;
146 }
147 i = j;
148 }
149 else if (last_space != cells.size() && last_space > line_start) {
150 // Break at the last space that fit on the line, swallowing that single space.
151 ranges.emplace_back(line_start, last_space);
152 i = last_space + 1;
153 }
154 else {
155 // No usable space boundary (an over-long word): hard-break at the column limit.
156 ranges.emplace_back(line_start, j);
157 i = j;
158 }
159 }
160
161 // Second pass: render each range, re-opening inherited style and closing any style left open.
162 std::vector<std::string> result;
163 result.reserve(ranges.size());
164 for (std::size_t r = 0; r < ranges.size(); ++r) {
165 const auto [start, end] = ranges[r];
166 std::string rendered = active_entering[start];
167 for (std::size_t k = start; k < end; ++k) {
168 rendered += cells[k].prefix;
169 rendered += cells[k].glyph;
170 }
171 const bool is_last = r + 1 == ranges.size();
172 if (is_last) {
173 rendered += trailing;
174 }
175 std::string active_at_end = active_entering[end];
176 if (is_last) {
177 apply_sgr(active_at_end, trailing);
178 }
179 if (!active_at_end.empty()) {
180 rendered += ansi_reset;
181 }
182 result.push_back(std::move(rendered));
183 }
184 return result;
185}
186
187} // namespace porytiles
std::size_t end
std::size_t start
std::vector< std::string > wrap_ansi_line(const std::string &line, std::size_t width)
Word-wraps a single logical line to a visible column width, preserving ANSI styling.