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, "&", "&amp;");
269             _replace_all_in_string(tmp, " ", "&nbsp;");
270             _replace_all_in_string(tmp, "<", "&lt;");
271             _replace_all_in_string(tmp, ">", "&gt;");
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