1 #include "LinkText.h"
2 
3 #include "ClientUI.h"
4 #include "CUIControls.h"
5 #include "../Empire/Empire.h"
6 #include "../universe/UniverseObject.h"
7 #include "../util/AppInterface.h"
8 #include "../util/Logger.h"
9 #include "../util/VarText.h"
10 #include "../util/i18n.h"
11 #include "../util/Directories.h"
12 #include "../universe/Enums.h"
13 
14 #include <GG/WndEvent.h>
15 #include <GG/GUI.h>
16 #include <boost/xpressive/xpressive.hpp>
17 #include <boost/algorithm/string.hpp>
18 
19 // TextLinker static(s)
20 const std::string TextLinker::ENCYCLOPEDIA_TAG("encyclopedia");
21 const std::string TextLinker::GRAPH_TAG("graph");
22 const std::string TextLinker::URL_TAG("url");
23 const std::string TextLinker::BROWSE_PATH_TAG("browsepath");
24 
25 namespace {
26     static const bool RENDER_DEBUGGING_LINK_RECTS = false;
27 
28     // closing format tag
29     static const std::string LINK_FORMAT_CLOSE = "</rgba>";
30 
AllParamsAsString(const std::vector<GG::Font::Substring> & params)31     std::string AllParamsAsString(const std::vector<GG::Font::Substring>& params) {
32         std::string retval;
33         for (const auto& param : params) {
34             if (!retval.empty())
35                 retval.append(" ");
36             retval.append(param);
37         }
38         return retval;
39     }
40 
ResolveNestedPathTypes(const std::string & text)41     std::string ResolveNestedPathTypes(const std::string& text) {
42         if (text.empty())
43             return text;
44         std::string new_text = text;
45         for (const auto& path_type : PathTypeStrings()) {
46             std::string path_string = PathToString(GetPath(path_type));
47             boost::replace_all(new_text, path_type, path_string);
48         }
49         return new_text;
50     }
51 
52     namespace xpr = boost::xpressive;
53     const xpr::sregex REGEX_NON_BRACKET = *~(xpr::set= '<', '>');
54     const std::string BROWSEPATH_TAG_OPEN_PRE("<" + TextLinker::BROWSE_PATH_TAG);
55     const std::string BROWSEPATH_TAG_CLOSE("</" + TextLinker::BROWSE_PATH_TAG + ">");
56     const xpr::sregex BROWSEPATH_SEARCH = BROWSEPATH_TAG_OPEN_PRE >> xpr::_s >> (xpr::s1 = REGEX_NON_BRACKET) >> ">" >>
57                                           (xpr::s2 = REGEX_NON_BRACKET) >> BROWSEPATH_TAG_CLOSE;
58 
59     /** Parses TextLinker::BROWSE_PATH_TAG%s within @p text, replacing string representation of PathType%s with
60      *  current path for that PathType.  If link label is empty, inserts resolved link argument as label */
BrowsePathLinkText(const std::string & text)61     std::string BrowsePathLinkText(const std::string& text) {
62         if (!boost::contains(text, BROWSEPATH_TAG_CLOSE))
63             return text;
64 
65         std::string retval(text);
66         auto text_it = retval.begin();
67         xpr::smatch match;
68         auto invalid_path_str = PathTypeToString(PATH_INVALID);
69 
70         while (true) {
71             if (!xpr::regex_search(text_it, retval.end(), match, BROWSEPATH_SEARCH, xpr::regex_constants::match_default))
72                 break;
73 
74             auto link_arg = ResolveNestedPathTypes(match[1]);
75             std::string link_label(match[2]);
76             if (link_label.empty())
77                 link_label = link_arg;
78             else
79                 link_label = ResolveNestedPathTypes(link_label);
80 
81             // Only support argument containing at least one valid PathType
82             if (link_arg == match[1].str() || boost::contains(link_arg, invalid_path_str)) {
83                 link_arg = invalid_path_str;
84                 link_label = UserString("ERROR") + ": " + link_label;
85             }
86 
87             auto resolved_link = BROWSEPATH_TAG_OPEN_PRE + " " + link_arg + ">" + link_label + BROWSEPATH_TAG_CLOSE;
88 
89             retval.replace(text_it + match.position(), text_it + match.position() + match.length(), resolved_link);
90 
91             text_it = retval.end() - match.suffix().length();
92         }
93 
94         return retval;
95     }
96 }
97 
98 ///////////////////////////////////////
99 // LinkText
100 ///////////////////////////////////////
LinkText(GG::X x,GG::Y y,GG::X w,const std::string & str,const std::shared_ptr<GG::Font> & font,GG::Flags<GG::TextFormat> format,GG::Clr color)101 LinkText::LinkText(GG::X x, GG::Y y, GG::X w, const std::string& str, const std::shared_ptr<GG::Font>& font,
102                    GG::Flags<GG::TextFormat> format/* = GG::FORMAT_NONE*/, GG::Clr color/* = GG::CLR_BLACK*/) :
103     GG::TextControl(x, y, w, GG::Y1, str, font, color, format, GG::INTERACTIVE),
104     TextLinker(),
105     m_raw_text(str)
106 {
107     Resize(TextLowerRight() - TextUpperLeft());
108     FindLinks();
109     MarkLinks();
110 }
111 
LinkText(GG::X x,GG::Y y,const std::string & str,const std::shared_ptr<GG::Font> & font,GG::Clr color)112 LinkText::LinkText(GG::X x, GG::Y y, const std::string& str, const std::shared_ptr<GG::Font>& font,
113                    GG::Clr color/* = GG::CLR_BLACK*/) :
114     GG::TextControl(x, y, GG::X1, GG::Y1, str, font, color, GG::FORMAT_NOWRAP, GG::INTERACTIVE),
115     TextLinker(),
116     m_raw_text(str)
117 {
118     FindLinks();
119     MarkLinks();
120 }
121 
Render()122 void LinkText::Render() {
123     GG::TextControl::Render();
124     TextLinker::Render_();
125 }
126 
SetText(const std::string & str)127 void LinkText::SetText(const std::string& str) {
128     m_raw_text = str;
129     FindLinks();
130     MarkLinks();
131 }
132 
SetLinkedText(const std::string & str)133 void LinkText::SetLinkedText(const std::string& str)
134 { GG::TextControl::SetText(str); }
135 
TextUpperLeft() const136 GG::Pt LinkText::TextUpperLeft() const
137 { return GG::TextControl::TextUpperLeft(); }
138 
TextLowerRight() const139 GG::Pt LinkText::TextLowerRight() const
140 { return GG::TextControl::TextLowerRight(); }
141 
GetLineData() const142 const std::vector<GG::Font::LineData>& LinkText::GetLineData() const
143 { return GG::TextControl::GetLineData(); }
144 
GetFont() const145 const std::shared_ptr<GG::Font>& LinkText::GetFont() const
146 { return GG::TextControl::GetFont(); }
147 
RawText() const148 const std::string& LinkText::RawText() const
149 { return m_raw_text; }
150 
LClick(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)151 void LinkText::LClick(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys)
152 { TextLinker::LClick_(pt, mod_keys); }
153 
RClick(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)154 void LinkText::RClick(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) {
155 
156     auto rclick_action = [this, pt, mod_keys]() { TextLinker::RClick_(pt, mod_keys); };
157     auto copy_action = [this]() { GG::GUI::GetGUI()->CopyWndText(this); };
158 
159     // create popup menu
160     auto popup = GG::Wnd::Create<CUIPopupMenu>(pt.x, pt.y);
161     if (GetLinkUnderPt(pt) != -1) {
162         popup->AddMenuItem(GG::MenuItem(UserString("OPEN"),     false, false, rclick_action));
163         popup->AddMenuItem(GG::MenuItem(true)); // separator
164     }
165     popup->AddMenuItem(GG::MenuItem(UserString("HOTKEY_COPY"),  false, false, copy_action));
166 
167     popup->Run();
168 }
169 
MouseHere(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)170 void LinkText::MouseHere(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys)
171 { TextLinker::MouseHere_(pt, mod_keys); }
172 
MouseLeave()173 void LinkText::MouseLeave()
174 { TextLinker::MouseLeave_(); }
175 
SizeMove(const GG::Pt & ul,const GG::Pt & lr)176 void LinkText::SizeMove(const GG::Pt& ul, const GG::Pt& lr) {
177     bool resized = Size() != (lr-ul);
178     GG::TextControl::SizeMove(ul, lr);
179     if (resized)
180         LocateLinks();
181 }
182 
183 ///////////////////////////////////////
184 // LinkDecorator
185 ///////////////////////////////////////
186 
Decorate(const std::string & target,const std::string & content) const187 std::string LinkDecorator::Decorate(const std::string& target, const std::string& content) const{
188     return GG::RgbaTag(ClientUI::DefaultLinkColor()) + content + LINK_FORMAT_CLOSE;
189 }
190 
DecorateRollover(const std::string & target,const std::string & content) const191 std::string LinkDecorator::DecorateRollover(const std::string& target, const std::string& content) const{
192     return GG::RgbaTag(ClientUI::RolloverLinkColor()) + content + LINK_FORMAT_CLOSE;
193 }
194 
CastStringToInt(const std::string & str)195 int LinkDecorator::CastStringToInt(const std::string& str) {
196     std::stringstream ss;
197     ss << str;
198     int retval = -1;
199     ss >> retval;
200 
201     if (ss.eof())
202         return retval;
203 
204     return -1;
205 }
206 
Decorate(const std::string & object_id_str,const std::string & content) const207 std::string ColorByOwner::Decorate(const std::string& object_id_str, const std::string& content) const {
208     GG::Clr color = ClientUI::DefaultLinkColor();
209     const Empire* empire = nullptr;
210     // get object indicated by object_id, and then get object's owner, if any
211     int object_id = CastStringToInt(object_id_str);
212     auto object = Objects().get(object_id);
213     if (object && !object->Unowned())
214         empire = GetEmpire(object->Owner());
215     if (empire)
216         color = empire->Color();
217     return GG::RgbaTag(color) + content + "</rgba>";
218 }
219 
Decorate(const std::string & path_type,const std::string & content) const220 std::string PathTypeDecorator::Decorate(const std::string& path_type, const std::string& content) const {
221     return LinkDecorator::Decorate(path_type, BrowsePathLinkText(content));
222 }
223 
DecorateRollover(const std::string & path_type,const std::string & content) const224 std::string PathTypeDecorator::DecorateRollover(const std::string& path_type, const std::string& content) const {
225     return LinkDecorator::DecorateRollover(path_type, BrowsePathLinkText(content));
226 }
227 
228 
229 ///////////////////////////////////////
230 // TextLinker::Link
231 ///////////////////////////////////////
232 struct TextLinker::Link {
233     std::string             type;           ///< contents of type field of link tag (eg "planet" in <planet 3>)
234     std::string             data;           ///< contents of data field of link tag (eg "3" in <planet 3>)
235     std::vector<GG::Rect>   rects;          ///< the rectangles in which this link falls, in window coordinates (some links may span more than one line)
236     std::pair<int, int>     text_posn;      ///< the index of the first (.first) and last + 1 (.second) characters in the raw link text
237     std::pair<int, int>     real_text_posn; ///< the index of the first and last + 1 characters in the current (potentially decorated) content string
238 };
239 
240 
241 ///////////////////////////////////////
242 // TextLinker
243 ///////////////////////////////////////
TextLinker()244 TextLinker::TextLinker() :
245     m_links(),
246     m_rollover_link(-1)
247 {
248     RegisterLinkTags();
249 }
250 
~TextLinker()251 TextLinker::~TextLinker()
252 {}
253 
SetDecorator(const std::string & link_type,LinkDecorator * decorator)254 void TextLinker::SetDecorator(const std::string& link_type, LinkDecorator* decorator) {
255     m_decorators[link_type] = std::shared_ptr<LinkDecorator>(decorator);
256     MarkLinks();
257 }
258 
LinkDefaultFormatTag(const Link & link,const std::string & content) const259 std::string TextLinker::LinkDefaultFormatTag(const Link& link, const std::string& content) const {
260     const LinkDecorator* decorator = &DEFAULT_DECORATOR;
261 
262     auto it = m_decorators.find(link.type);
263     if (it != m_decorators.end()){
264         decorator = it->second.get();
265     }
266 
267     return decorator->Decorate(link.data, content);
268 }
269 
LinkRolloverFormatTag(const Link & link,const std::string & content) const270 std::string TextLinker::LinkRolloverFormatTag(const Link& link, const std::string& content) const {
271     const LinkDecorator* decorator = &DEFAULT_DECORATOR;
272 
273     auto it = m_decorators.find(link.type);
274     if (it != m_decorators.end())
275         decorator = it->second.get();
276 
277     return decorator->DecorateRollover(link.data, content);
278 }
279 
Render_()280 void TextLinker::Render_() {
281     if (!RENDER_DEBUGGING_LINK_RECTS)
282         return;
283 
284     // draw yellow box around whole text block
285     GG::Rect bounds(TextUpperLeft(), TextLowerRight());
286     FlatRectangle(bounds.ul, bounds.lr, GG::CLR_ZERO, GG::CLR_YELLOW, 1);
287 
288     // draw red box around individual linkified bits of text within block
289     for (const Link& link : m_links) {
290         for (const GG::Rect& rect : link.rects) {
291             GG::Rect r = TextUpperLeft() + rect;
292             FlatRectangle(r.ul, r.lr, GG::CLR_ZERO, GG::CLR_RED, 1);
293         }
294     }
295 }
296 
LClick_(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)297 void TextLinker::LClick_(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) {
298     int sel_link = GetLinkUnderPt(pt);
299     if (sel_link == -1)
300         return;
301     if (sel_link < 0 || sel_link >= static_cast<int>(m_links.size())) {
302         ErrorLogger() << "TextLinker::LClick_ found out of bounds sel_link!";
303         return;
304     }
305 
306     const std::string LINK_TYPE = m_links[sel_link].type;
307     const std::string DATA = m_links[sel_link].data;
308 
309     LinkClickedSignal(LINK_TYPE, DATA);
310 }
311 
RClick_(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)312 void TextLinker::RClick_(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) {
313     int sel_link = GetLinkUnderPt(pt);
314     if (sel_link == -1)
315         return;
316     if (sel_link < 0 || sel_link >= static_cast<int>(m_links.size())) {
317         ErrorLogger() << "TextLinker::RClick_ found out of bounds sel_link!";
318         return;
319     }
320 
321     const std::string& LINK_TYPE = m_links[sel_link].type;
322     const std::string& DATA = m_links[sel_link].data;
323 
324     LinkRightClickedSignal(LINK_TYPE, DATA);
325 }
326 
LDoubleClick_(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)327 void TextLinker::LDoubleClick_(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) {
328     int sel_link = GetLinkUnderPt(pt);
329     if (sel_link == -1)
330         return;
331     if (sel_link < 0 || sel_link >= static_cast<int>(m_links.size())) {
332         ErrorLogger() << "TextLinker::DoubleClick_ found out of bounds sel_link!";
333         return;
334     }
335 
336     const std::string& LINK_TYPE = m_links[sel_link].type;
337     const std::string& DATA = m_links[sel_link].data;
338 
339     LinkDoubleClickedSignal(LINK_TYPE, DATA);
340 }
341 
MouseHere_(const GG::Pt & pt,GG::Flags<GG::ModKey> mod_keys)342 void TextLinker::MouseHere_(const GG::Pt& pt, GG::Flags<GG::ModKey> mod_keys) {
343     int rollover_link = GetLinkUnderPt(pt);
344     if (rollover_link != m_rollover_link) {
345         m_rollover_link = rollover_link;
346         MarkLinks();
347     }
348 }
349 
MouseLeave_()350 void TextLinker::MouseLeave_() {
351     m_rollover_link = -1;
352     MarkLinks();
353 }
354 
FindLinks()355 void TextLinker::FindLinks() {
356     m_links.clear();
357 
358     GG::Y y_posn(0); // y-coordinate of the top of the current line
359     Link link;
360 
361     // control text needs to be updated so that the line data is calculated from
362     // the raw text and not the marked text set in MarkText().
363     SetLinkedText(RawText());
364 
365     for (const auto& curr_line : GetLineData()) {
366         for (const auto& curr_char : curr_line.char_data) {
367             for (const auto& tag : curr_char.tags) {
368                 if (tag->tag_name == VarText::PLANET_ID_TAG ||
369                     tag->tag_name == VarText::SYSTEM_ID_TAG ||
370                     tag->tag_name == VarText::SHIP_ID_TAG ||
371                     tag->tag_name == VarText::FLEET_ID_TAG ||
372                     tag->tag_name == VarText::BUILDING_ID_TAG ||
373                     tag->tag_name == VarText::FIELD_ID_TAG ||
374                     tag->tag_name == VarText::COMBAT_ID_TAG ||
375                     tag->tag_name == VarText::EMPIRE_ID_TAG ||
376                     tag->tag_name == VarText::DESIGN_ID_TAG ||
377                     tag->tag_name == VarText::PREDEFINED_DESIGN_TAG ||
378                     tag->tag_name == VarText::TECH_TAG ||
379                     tag->tag_name == VarText::BUILDING_TYPE_TAG ||
380                     tag->tag_name == VarText::SPECIAL_TAG ||
381                     tag->tag_name == VarText::SHIP_HULL_TAG ||
382                     tag->tag_name == VarText::SHIP_PART_TAG ||
383                     tag->tag_name == VarText::SPECIES_TAG ||
384                     tag->tag_name == VarText::FIELD_TYPE_TAG ||
385                     tag->tag_name == VarText::METER_TYPE_TAG ||
386                     tag->tag_name == TextLinker::ENCYCLOPEDIA_TAG ||
387                     tag->tag_name == TextLinker::GRAPH_TAG ||
388                     tag->tag_name == TextLinker::URL_TAG ||
389                     tag->tag_name == TextLinker::BROWSE_PATH_TAG)
390                 {
391                     link.type = tag->tag_name;
392                     if (tag->close_tag) {
393                         link.text_posn.second = Value(curr_char.string_index);
394                         m_links.push_back(link);
395                         link = Link();
396                     } else {
397                         if (!tag->params.empty()) {
398                             if (tag->tag_name == TextLinker::BROWSE_PATH_TAG) {
399                                 auto all_param_str(AllParamsAsString(tag->params));
400                                 link.data = ResolveNestedPathTypes(all_param_str);
401                                 // BROWSE_PATH_TAG requires a PathType within param
402                                 if (link.data == all_param_str) {
403                                     ErrorLogger() << "Invalid param \"" << link.data << "\" for tag "
404                                                   << TextLinker::BROWSE_PATH_TAG;
405                                     link.data = PathTypeToString(PATH_INVALID);
406                                 }
407                             } else {
408                                 link.data = tag->params[0];
409                             }
410                         } else {
411                             link.data.clear();
412                         }
413                         link.text_posn.first = Value(curr_char.string_index);
414                         for (auto& itag : curr_char.tags) {
415                             link.text_posn.first -= Value(itag->StringSize());
416                         }
417                     }
418                     // Before decoration, the real positions are the same as the raw ones
419                     link.real_text_posn = link.text_posn;
420                 }
421             }
422         }
423     }
424 
425     LocateLinks();
426 }
427 
LocateLinks()428 void TextLinker::LocateLinks() {
429     if (m_links.empty())
430         return;
431 
432     GG::Y y_posn(0); // y-coordinate of the top of the current line
433     const auto& font = GetFont();
434 
435     // We assume that links are stored in m_links in the order they appear in the text.
436     // We shall iterate through the text, updating the rectangles of a link whenever we know we are inside it
437     auto current_link = m_links.begin();
438     bool inside_link = false;
439 
440     for (const auto& curr_line : GetLineData()) {
441         // if the last line ended without the current tag ending
442         if (inside_link)
443             current_link->rects.push_back(GG::Rect(GG::X0, y_posn, GG::X0,
444                                                    y_posn + font->Height()));
445 
446         for (unsigned int i = 0; i < curr_line.char_data.size(); ++i) {
447             // The link text_posn is at the beginning of the tag, whereas
448             // char_data jumps over tags. That is why we cannot test for precise equality
449             if (!inside_link && curr_line.char_data[i].string_index >= current_link->real_text_posn.first &&
450                 curr_line.char_data[i].string_index < current_link->real_text_posn.second)
451             {
452                 inside_link = true;
453                 // Clear out the old rectangles
454                 current_link->rects.clear();
455                 current_link->rects.push_back(GG::Rect(i ? curr_line.char_data[i - 1].extent : GG::X0,
456                                               y_posn, GG::X0, y_posn + font->Height()));
457             } else if (inside_link && curr_line.char_data[i].string_index >= current_link->real_text_posn.second) {
458                 inside_link = false;
459                 current_link->rects.back().lr.x = i ? curr_line.char_data[i - 1].extent : GG::X0;
460                 ++current_link;
461                 if (current_link == m_links.end())
462                     return;
463             }
464         }
465 
466         // if a line is ending without the current tag ending
467         if (inside_link)
468             current_link->rects.back().lr.x = curr_line.char_data.empty() ? GG::X0 : curr_line.char_data.back().extent;
469 
470         y_posn += font->Lineskip();
471     }
472 }
473 
GetLinkUnderPt(const GG::Pt & pt)474 int TextLinker::GetLinkUnderPt(const GG::Pt& pt) {
475     std::vector<Link> links;
476     try {
477         links = m_links;
478     } catch (...) {
479         ErrorLogger() << "exception caught copying links in GetLinkUnderPt";
480         return -1;
481     }
482 
483     GG::Pt tex_ul = TextUpperLeft();
484 
485     for (unsigned int i = 0; i < links.size(); ++i) {
486         const Link& link = links[i];
487 
488         for (const GG::Rect& link_rect : link.rects) {
489             GG::Rect r = tex_ul + link_rect;
490             if (r.Contains(pt))
491                 return i;
492         }
493     }
494     return -1;  // no link found
495 }
496 
MarkLinks()497 void TextLinker::MarkLinks() {
498     const std::string& raw_text = RawText();
499 
500     if (m_links.empty()) {
501         SetLinkedText(raw_text);
502         return;
503     }
504 
505     int copy_start_index = 0;
506     auto raw_text_start_it = raw_text.begin();
507 
508     std::string marked_text;
509 
510     // copy text from current copy_start_index up to just before start of next link
511     for (int i = 0; i < static_cast<int>(m_links.size()); ++i) {
512         Link& link = m_links[i];
513         int link_start_index = link.text_posn.first;
514         int link_end_index = link.text_posn.second;
515 
516         // copy raw text up to start of first link
517         std::copy(raw_text_start_it + copy_start_index, raw_text_start_it + link_start_index, std::back_inserter(marked_text));
518 
519         std::string content = std::string(raw_text_start_it + link_start_index, raw_text_start_it + link_end_index);
520 
521         int length_before = marked_text.size();
522         // add link markup open tag
523         if (i == m_rollover_link)
524             marked_text += LinkRolloverFormatTag(link, content);
525         else
526             marked_text += LinkDefaultFormatTag(link, content);
527 
528         // update copy point for following text
529         copy_start_index = link_end_index;
530         // update m_links to know the real positions of the links in the decorated text
531         // this makes you able to call LocateLinks without resetting the text.
532         link.real_text_posn.first = length_before;
533         link.real_text_posn.second = marked_text.size();
534     }
535 
536     // copy remaining text after last link
537     std::copy(raw_text_start_it + copy_start_index, raw_text.end(), std::back_inserter(marked_text));
538 
539     // set underlying UI control text
540     SetLinkedText(marked_text);
541 }
542 
543 const LinkDecorator TextLinker::DEFAULT_DECORATOR;
544 
LinkTaggedText(const std::string & tag,const std::string & stringtable_entry)545 std::string LinkTaggedText(const std::string& tag, const std::string& stringtable_entry)
546 { return "<" + tag + " " + stringtable_entry + ">" + UserString(stringtable_entry) + "</" + tag + ">"; }
547 
LinkTaggedIDText(const std::string & tag,int id,const std::string & text)548 std::string LinkTaggedIDText(const std::string& tag, int id, const std::string& text)
549 { return "<" + tag + " " + std::to_string(id) + ">" + text + "</" + tag + ">"; }
550 
LinkTaggedPresetText(const std::string & tag,const std::string & stringtable_entry,const std::string & display_text)551 std::string LinkTaggedPresetText(const std::string& tag, const std::string& stringtable_entry, const std::string& display_text)
552 { return "<" + tag + " " + stringtable_entry + ">" + display_text + "</" + tag + ">"; }
553 
554 namespace {
555     static bool link_tags_registered = false;
556 }
557 
RegisterLinkTags()558 void RegisterLinkTags() {
559     if (link_tags_registered)
560         return;
561     link_tags_registered = true;
562 
563     // need to register the tags that link text uses so GG::Font will know how to (not) render them
564     GG::Font::RegisterKnownTag(VarText::PLANET_ID_TAG);
565     GG::Font::RegisterKnownTag(VarText::SYSTEM_ID_TAG);
566     GG::Font::RegisterKnownTag(VarText::SHIP_ID_TAG);
567     GG::Font::RegisterKnownTag(VarText::FLEET_ID_TAG);
568     GG::Font::RegisterKnownTag(VarText::BUILDING_ID_TAG);
569     GG::Font::RegisterKnownTag(VarText::FIELD_ID_TAG);
570 
571     GG::Font::RegisterKnownTag(VarText::COMBAT_ID_TAG);
572 
573     GG::Font::RegisterKnownTag(VarText::EMPIRE_ID_TAG);
574     GG::Font::RegisterKnownTag(VarText::DESIGN_ID_TAG);
575     GG::Font::RegisterKnownTag(VarText::PREDEFINED_DESIGN_TAG);
576 
577     GG::Font::RegisterKnownTag(VarText::TECH_TAG);
578     GG::Font::RegisterKnownTag(VarText::BUILDING_TYPE_TAG);
579     GG::Font::RegisterKnownTag(VarText::SPECIAL_TAG);
580     GG::Font::RegisterKnownTag(VarText::SHIP_HULL_TAG);
581     GG::Font::RegisterKnownTag(VarText::SHIP_PART_TAG);
582     GG::Font::RegisterKnownTag(VarText::SPECIES_TAG);
583     GG::Font::RegisterKnownTag(VarText::FIELD_TYPE_TAG);
584     GG::Font::RegisterKnownTag(VarText::METER_TYPE_TAG);
585 
586     GG::Font::RegisterKnownTag(TextLinker::ENCYCLOPEDIA_TAG);
587     GG::Font::RegisterKnownTag(TextLinker::GRAPH_TAG);
588     GG::Font::RegisterKnownTag(TextLinker::URL_TAG);
589     GG::Font::RegisterKnownTag(TextLinker::BROWSE_PATH_TAG);
590 }
591