1 #include "AppHdr.h"
2
3 #include "format.h"
4
5 #include <climits>
6
7 #include "cio.h"
8 #include "colour.h"
9 #include "lang-fake.h"
10 #include "libutil.h"
11 #include "stringutil.h"
12 #include "unicode.h"
13 #include "viewchar.h"
14
formatted_string(int init_colour)15 formatted_string::formatted_string(int init_colour)
16 : ops()
17 {
18 if (init_colour)
19 textcolour(init_colour);
20 }
21
formatted_string(const string & s,int init_colour)22 formatted_string::formatted_string(const string &s, int init_colour)
23 : ops()
24 {
25 if (init_colour)
26 textcolour(init_colour);
27 cprintf(s);
28 }
29
30 /**
31 * For a given tag, return the corresponding colour.
32 *
33 * @param tag The tag: e.g. "red", "lightblue", "h", "w".
34 * @return The corresponding colour (e.g. RED), or LIGHTGREY.
35 */
get_colour(const string & tag)36 int formatted_string::get_colour(const string &tag)
37 {
38 if (tag == "h")
39 return YELLOW;
40
41 if (tag == "w")
42 return WHITE;
43
44 return str_to_colour(tag);
45 }
46
47 // Display a formatted string without printing literal \n.
48 // This is important if the text is not supposed
49 // to clobber existing text to the right of the lines being displayed
50 // (some of the tutorial messages need this).
display_tagged_block(const string & s)51 void display_tagged_block(const string &s)
52 {
53 vector<formatted_string> lines;
54 formatted_string::parse_string_to_multiple(s, lines);
55
56 int x = wherex();
57 int y = wherey();
58 const unsigned int max_y = cgetsize(GOTO_CRT).y;
59 const int size = min<unsigned int>(lines.size(), max_y - y + 1);
60 for (int i = 0; i < size; ++i)
61 {
62 cgotoxy(x, y);
63 lines[i].display();
64 y++;
65 }
66 }
67
68 /**
69 * Take a string and turn it into a formatted_string.
70 *
71 * @param s The input string: e.g. "<red>foo</red>".
72 * @param main_colour The initial & default text colour.
73 * @return A formatted string corresponding to the input.
74 */
parse_string(const string & s,int main_colour)75 formatted_string formatted_string::parse_string(const string &s,
76 int main_colour)
77 {
78 // main_colour will usually be LIGHTGREY (default).
79 vector<int> colour_stack(1, main_colour);
80
81 formatted_string fs;
82
83 parse_string1(s, fs, colour_stack);
84 if (colour_stack.back() != colour_stack.front())
85 fs.textcolour(colour_stack.front()); // XXX: this does nothing
86 return fs;
87 }
88
89 // Parses a formatted string in much the same way as parse_string, but
90 // handles \n by creating a new formatted_string.
parse_string_to_multiple(const string & s,vector<formatted_string> & out,int wrap_col)91 void formatted_string::parse_string_to_multiple(const string &s,
92 vector<formatted_string> &out,
93 int wrap_col)
94 {
95 vector<int> colour_stack(1, LIGHTGREY);
96
97 vector<string> lines = split_string("\n", s, false, true);
98 if (wrap_col > 0)
99 {
100 vector<string> pre_split = move(lines);
101 for (string &line : pre_split)
102 {
103 do
104 {
105 lines.push_back(wordwrap_line(line, wrap_col, true, true));
106 }
107 while (!line.empty());
108 }
109 }
110
111 for (const string &line : lines)
112 {
113 out.emplace_back();
114 formatted_string& fs = out.back();
115 fs.textcolour(colour_stack.back());
116 parse_string1(line, fs, colour_stack);
117 if (colour_stack.back() != colour_stack.front())
118 fs.textcolour(colour_stack.front()); // XXX: this does nothing
119 }
120 }
121
122 // Helper for the other parse_ methods.
parse_string1(const string & s,formatted_string & fs,vector<int> & colour_stack)123 void formatted_string::parse_string1(const string &s, formatted_string &fs,
124 vector<int> &colour_stack)
125 {
126 // FIXME: This is a lame mess, just good enough for the task on hand
127 // (keyboard help).
128 string::size_type tag = string::npos;
129 string::size_type length = s.length();
130
131 string currs;
132
133 for (tag = 0; tag < length; ++tag)
134 {
135 bool revert_colour = false;
136 string::size_type endpos = string::npos;
137
138 // Break string up if it gets too big.
139 if (currs.size() >= 999)
140 {
141 // Break the string at the end of a line, if possible, so
142 // that none of the broken string ends up overwritten.
143 string::size_type bound = currs.rfind("\n", 999);
144 if (bound != endpos)
145 bound++;
146 else
147 bound = 999;
148
149 fs.cprintf(currs.substr(0, bound));
150 if (currs.size() > bound)
151 currs = currs.substr(bound);
152 else
153 currs.clear();
154 tag--;
155 continue;
156 }
157
158 if (s[tag] != '<' || tag >= length - 1)
159 {
160 currs += s[tag];
161 continue;
162 }
163
164 // Is this a << escape?
165 if (s[tag + 1] == '<')
166 {
167 currs += s[tag];
168 tag++;
169 continue;
170 }
171
172 endpos = s.find('>', tag + 1);
173 // No closing >?
174 if (endpos == string::npos)
175 {
176 currs += s[tag];
177 continue;
178 }
179
180 string tagtext = s.substr(tag + 1, endpos - tag - 1);
181 if (tagtext.empty() || tagtext == "/")
182 {
183 currs += s[tag];
184 continue;
185 }
186
187 if (tagtext[0] == '/')
188 {
189 revert_colour = true;
190 tagtext = tagtext.substr(1);
191 tag++;
192 }
193
194 if (!currs.empty())
195 {
196 fs.cprintf(currs);
197 currs.clear();
198 }
199
200 if (revert_colour)
201 {
202 const int endcolour = get_colour(tagtext);
203
204 if (colour_stack.size() > 1 && endcolour == colour_stack.back())
205 colour_stack.pop_back();
206 else
207 {
208 // If this was the only tag, or the colour didn't match
209 // the one we are popping, display the tag as a warning.
210 fs.textcolour(LIGHTRED);
211 fs.cprintf("</%s>", tagtext.c_str());
212 }
213 }
214 else
215 {
216 const int colour = get_colour(tagtext);
217 if (colour == -1)
218 {
219 fs.textcolour(LIGHTRED);
220 fs.cprintf("<%s>", tagtext.c_str());
221 }
222 else
223 colour_stack.push_back(colour);
224 }
225
226 // fs.cprintf("%d%d", colour_stack.size(), colour_stack.back());
227 fs.textcolour(colour_stack.back());
228
229 tag += tagtext.length() + 1;
230 }
231 if (currs.length())
232 fs.cprintf(currs);
233 }
234
235 /// Return a plaintext version of this string, sans tags, colours, etc.
operator string() const236 formatted_string::operator string() const
237 {
238 string s;
239 for (const fs_op &op : ops)
240 if (op.type == FSOP_TEXT)
241 s += op.text;
242
243 return s;
244 }
245
_replace_all_in_string(string & s,const string & search,const string & replace)246 static void _replace_all_in_string(string& s, const string& search,
247 const string& replace)
248 {
249 string::size_type pos = 0;
250 while ((pos = s.find(search, pos)) != string::npos)
251 {
252 s.replace(pos, search.size(), replace);
253 pos += replace.size();
254 }
255 }
256
html_dump() const257 string formatted_string::html_dump() const
258 {
259 string s;
260 for (const fs_op &op : ops)
261 {
262 string tmp;
263 switch (op.type)
264 {
265 case FSOP_TEXT:
266 tmp = op.text;
267 // (very) crude HTMLification
268 _replace_all_in_string(tmp, "&", "&");
269 _replace_all_in_string(tmp, " ", " ");
270 _replace_all_in_string(tmp, "<", "<");
271 _replace_all_in_string(tmp, ">", ">");
272 _replace_all_in_string(tmp, "\n", "<br>");
273 s += tmp;
274 break;
275 case FSOP_COLOUR:
276 s += "<font color=";
277 s += colour_to_str(op.colour);
278 s += ">";
279 break;
280 }
281 }
282 return s;
283 }
284
operator <(const formatted_string & other) const285 bool formatted_string::operator < (const formatted_string &other) const
286 {
287 return string(*this) < string(other);
288 }
289
operator ==(const formatted_string & other) const290 bool formatted_string::operator == (const formatted_string &other) const
291 {
292 // May produce false negative in some cases, e.g. duplicated colour ops
293 return ops == other.ops;
294 }
295
296 const formatted_string &
operator +=(const formatted_string & other)297 formatted_string::operator += (const formatted_string &other)
298 {
299 ops.insert(ops.end(), other.ops.begin(), other.ops.end());
300 return *this;
301 }
302
303 const formatted_string &
operator +=(const string & other)304 formatted_string::operator += (const string& other)
305 {
306 ops.emplace_back(other);
307 return *this;
308 }
309
width() const310 int formatted_string::width() const
311 {
312 // Just add up the individual string lengths.
313 int len = 0;
314 for (const fs_op &op : ops)
315 if (op.type == FSOP_TEXT)
316 len += strwidth(op.text);
317 return len;
318 }
319
cap(int & i,int max)320 static inline void cap(int &i, int max)
321 {
322 if (i < 0 && -i <= max)
323 i += max;
324 if (i >= max)
325 i = max - 1;
326 if (i < 0)
327 i = 0;
328 }
329
operator [](size_t idx)330 char &formatted_string::operator [] (size_t idx)
331 {
332 size_t rel_idx = idx;
333 int size = ops.size();
334 for (int i = 0; i < size; ++i)
335 {
336 if (ops[i].type != FSOP_TEXT)
337 continue;
338
339 size_t len = ops[i].text.length();
340 if (rel_idx >= len)
341 rel_idx -= len;
342 else
343 return ops[i].text[rel_idx];
344 }
345 die("Invalid index");
346 }
347
tostring(int s,int e) const348 string formatted_string::tostring(int s, int e) const
349 {
350 string st;
351
352 int size = ops.size();
353 cap(s, size);
354 cap(e, size);
355
356 for (int i = s; i <= e && i < size; ++i)
357 {
358 if (ops[i].type == FSOP_TEXT)
359 st += ops[i].text;
360 }
361 return st;
362 }
363
to_colour_string() const364 string formatted_string::to_colour_string() const
365 {
366 string st;
367 const int size = ops.size();
368 for (int i = 0; i < size; ++i)
369 {
370 if (ops[i].type == FSOP_TEXT)
371 {
372 // gotta double up those '<' chars ...
373 size_t start = st.size();
374 st += ops[i].text;
375
376 while (true)
377 {
378 const size_t left_angle = st.find('<', start);
379 if (left_angle == string::npos)
380 break;
381
382 st.insert(left_angle, "<");
383 start = left_angle + 2;
384 }
385 }
386 else if (ops[i].type == FSOP_COLOUR)
387 {
388 st += "<";
389 st += colour_to_str(ops[i].colour);
390 st += ">";
391 }
392 }
393
394 return st;
395 }
396
display(int s,int e) const397 void formatted_string::display(int s, int e) const
398 {
399 int size = ops.size();
400 if (!size)
401 return;
402
403 cap(s, size);
404 cap(e, size);
405
406 for (int i = s; i <= e && i < size; ++i)
407 ops[i].display();
408 }
409
find_last_colour() const410 int formatted_string::find_last_colour() const
411 {
412 if (!ops.empty())
413 {
414 for (int i = ops.size() - 1; i >= 0; --i)
415 if (ops[i].type == FSOP_COLOUR)
416 return ops[i].colour;
417 }
418 return LIGHTGREY;
419 }
420
chop(int length,bool pad) const421 formatted_string formatted_string::chop(int length, bool pad) const
422 {
423 formatted_string result;
424 for (const fs_op& op : ops)
425 {
426 if (op.type == FSOP_TEXT)
427 {
428 result.ops.push_back(op);
429 string& new_string = result.ops[result.ops.size()-1].text;
430 int w = strwidth(new_string);
431 if (w > length)
432 new_string = chop_string(new_string, length, false);
433 length -= w;
434 if (length <= 0)
435 break;
436 }
437 else
438 result.ops.push_back(op);
439 }
440 if (pad && length > 0)
441 result += string(length, ' ');
442
443 return result;
444 }
445
chop_bytes(int length) const446 formatted_string formatted_string::chop_bytes(int length) const
447 {
448 return substr_bytes(0, length);
449 }
450
substr_bytes(int pos,int length) const451 formatted_string formatted_string::substr_bytes(int pos, int length) const
452 {
453 formatted_string result;
454 fs_op initial(LIGHTGREY);
455 for (const fs_op& op : ops)
456 {
457 if (op.type == FSOP_TEXT)
458 {
459 int n = op.text.size();
460 if (pos >= n)
461 {
462 pos -= n;
463 continue;
464 }
465 if (result.empty())
466 result.ops.push_back(initial);
467 result.ops.push_back(fs_op(op.text.substr(pos, length)));
468 string& new_string = result.ops[result.ops.size()-1].text;
469 pos = 0;
470 length -= new_string.size();
471 if (length <= 0)
472 break;
473 }
474 else if (pos == 0)
475 result.ops.push_back(op);
476 else
477 initial = op;
478 }
479 return result;
480 }
481
trim() const482 formatted_string formatted_string::trim() const
483 {
484 return parse_string(trimmed_string(to_colour_string()));
485 }
486
del_char()487 void formatted_string::del_char()
488 {
489 for (auto i = ops.begin(); i != ops.end(); ++i)
490 {
491 if (i->type != FSOP_TEXT)
492 continue;
493 switch (strwidth(i->text))
494 {
495 case 0: // shouldn't happen
496 continue;
497 case 1:
498 ops.erase(i);
499 return;
500 }
501 i->text = next_glyph((char*)i->text.c_str());
502 return;
503 }
504 }
505
add_glyph(cglyph_t g)506 void formatted_string::add_glyph(cglyph_t g)
507 {
508 const int last_col = find_last_colour();
509 textcolour(g.col);
510 cprintf("%s", stringize_glyph(g.ch).c_str());
511 textcolour(last_col);
512 }
513
textcolour(int colour)514 void formatted_string::textcolour(int colour)
515 {
516 if (!ops.empty() && ops.back().type == FSOP_COLOUR)
517 ops.pop_back();
518
519 ops.emplace_back(colour);
520 }
521
clear()522 void formatted_string::clear()
523 {
524 ops.clear();
525 }
526
empty() const527 bool formatted_string::empty() const
528 {
529 return ops.empty();
530 }
531
cprintf(const char * s,...)532 void formatted_string::cprintf(const char *s, ...)
533 {
534 va_list args;
535 va_start(args, s);
536 cprintf(vmake_stringf(s, args));
537 va_end(args);
538 }
539
cprintf(const string & s)540 void formatted_string::cprintf(const string &s)
541 {
542 ops.push_back(s);
543 }
544
display() const545 void formatted_string::fs_op::display() const
546 {
547 switch (type)
548 {
549 case FSOP_COLOUR:
550 #ifndef USE_TILE_LOCAL
551 if (colour < NUM_TERM_COLOURS)
552 #endif
553 ::textcolour(colour);
554 break;
555 case FSOP_TEXT:
556 wrapcprintf("%s", text.c_str());
557 break;
558 }
559 }
560
swap(formatted_string & other)561 void formatted_string::swap(formatted_string& other)
562 {
563 ops.swap(other.ops);
564 }
565
all_caps()566 void formatted_string::all_caps()
567 {
568 for (fs_op &op : ops)
569 if (op.type == FSOP_TEXT)
570 uppercase(op.text);
571 }
572
capitalise()573 void formatted_string::capitalise()
574 {
575 for (fs_op &op : ops)
576 if (op.type == FSOP_TEXT && !op.text.empty())
577 {
578 op.text = uppercase_first(op.text);
579 break;
580 }
581 }
582
filter_lang()583 void formatted_string::filter_lang()
584 {
585 for (fs_op &op : ops)
586 if (op.type == FSOP_TEXT)
587 ::filter_lang(op.text);
588 }
589
count_linebreaks(const formatted_string & fs)590 int count_linebreaks(const formatted_string& fs)
591 {
592 string::size_type where = 0;
593 const string s = fs;
594 int count = 0;
595 while (1)
596 {
597 where = s.find("\n", where);
598 if (where == string::npos)
599 break;
600 else
601 {
602 ++count;
603 ++where;
604 }
605 }
606 return count;
607 }
608