The Battle for Wesnoth  1.17.0-dev
string_utils.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2005 - 2021
3  by Guillaume Melquiond <guillaume.melquiond@gmail.com>
4  Copyright (C) 2003 by David White <dave@whitevine.net>
5  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
6 
7  This program is free software; you can redistribute it and/or modify
8  it under the terms of the GNU General Public License as published by
9  the Free Software Foundation; either version 2 of the License, or
10  (at your option) any later version.
11  This program is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY.
13 
14  See the COPYING file for more details.
15 */
16 
17 #define GETTEXT_DOMAIN "wesnoth-lib"
18 
19 #include "formula/string_utils.hpp"
20 #include "variable.hpp"
21 
22 #include "config.hpp"
23 #include "log.hpp"
24 #include "gettext.hpp"
25 
26 static lg::log_domain log_engine("engine");
27 #define ERR_NG LOG_STREAM(err, log_engine)
28 #define WRN_NG LOG_STREAM(warn, log_engine)
29 
30 static bool two_dots(char a, char b) { return a == '.' && b == '.'; }
31 
32 namespace utils {
33 
34  namespace detail {
35  std::string(* evaluate_formula)(const std::string& formula) = nullptr;
36  }
37 
38 template <typename T>
40 {
41 public:
42  string_map_variable_set(const std::map<std::string,T>& map) : map_(map) {}
43 
44  virtual config::attribute_value get_variable_const(const std::string &key) const
45  {
47  const auto itor = map_.find(key);
48  if (itor != map_.end())
49  val = itor->second;
50  return val;
51  }
52  virtual variable_access_const get_variable_access_read(const std::string& varname) const
53  {
54  temp_.reset(new config);
55  for(const auto& p : map_) {
56  temp_->insert(p.first, p.second);
57  }
58  return variable_access_const(varname, *temp_);
59  }
60 private:
61  const std::map<std::string,T>& map_;
62  mutable std::shared_ptr<config> temp_; // only used if get_variable_access_read called
63 };
64 }
65 
66 static std::string do_interpolation(const std::string &str, const variable_set& set)
67 {
68  std::string res = str;
69  // This needs to be able to store negative numbers to check for the while's condition
70  // (which is only false when the previous '$' was at index 0)
71  int rfind_dollars_sign_from = res.size();
72  while(rfind_dollars_sign_from >= 0) {
73  // Going in a backwards order allows nested variable-retrieval, e.g. in arrays.
74  // For example, "I am $creatures[$i].user_description!"
75  const std::string::size_type var_begin_loc = res.rfind('$', rfind_dollars_sign_from);
76 
77  // If there are no '$' left then we're done.
78  if(var_begin_loc == std::string::npos) {
79  break;
80  }
81 
82  // For the next iteration of the loop, search for more '$'
83  // (not from the same place because sometimes the '$' is not replaced)
84  rfind_dollars_sign_from = static_cast<int>(var_begin_loc) - 1;
85 
86 
87  const std::string::iterator var_begin = res.begin() + var_begin_loc;
88 
89  // The '$' is not part of the variable name.
90  const std::string::iterator var_name_begin = var_begin + 1;
91  std::string::iterator var_end = var_name_begin;
92 
93  if(var_name_begin == res.end()) {
94  // Any '$' at the end of a string is just a '$'
95  continue;
96  } else if(*var_name_begin == '(') {
97  // The $( ... ) syntax invokes a formula
98  int paren_nesting_level = 0;
99  bool in_string = false,
100  in_comment = false;
101  do {
102  switch(*var_end) {
103  case '(':
104  if(!in_string && !in_comment) {
105  ++paren_nesting_level;
106  }
107  break;
108  case ')':
109  if(!in_string && !in_comment) {
110  --paren_nesting_level;
111  }
112  break;
113  case '#':
114  if(!in_string) {
115  in_comment = !in_comment;
116  }
117  break;
118  case '\'':
119  if(!in_comment) {
120  in_string = !in_string;
121  }
122  break;
123  // TODO: support escape sequences when/if they are allowed in FormulaAI strings
124  }
125  } while(++var_end != res.end() && paren_nesting_level > 0);
126  if(utils::detail::evaluate_formula == nullptr) {
127  WRN_NG << "Formula substitution ignored (and removed) because WFL engine is not present in the server.\n";
128  res.replace(var_begin, var_end, "");
129  continue;
130  }
131  if(paren_nesting_level > 0) {
132  ERR_NG << "Formula in WML string cannot be evaluated due to "
133  << "a missing closing parenthesis:\n\t--> \""
134  << std::string(var_begin, var_end) << "\"\n";
135  res.replace(var_begin, var_end, "");
136  continue;
137  }
138  res.replace(var_begin, var_end, utils::detail::evaluate_formula(std::string(var_begin+2, var_end-1)));
139  continue;
140  }
141 
142  // Find the maximum extent of the variable name (it may be shortened later).
143  for(int bracket_nesting_level = 0; var_end != res.end(); ++var_end) {
144  const char c = *var_end;
145  if(c == '[') {
146  ++bracket_nesting_level;
147  }
148  else if(c == ']') {
149  if(--bracket_nesting_level < 0) {
150  break;
151  }
152  }
153  // isascii() breaks on mingw with -std=c++0x
154  else if (!(((c) & ~0x7f) == 0)/*isascii(c)*/ || (!isalnum(c) && c != '.' && c != '_')) {
155  break;
156  }
157  }
158 
159  // Two dots in a row cannot be part of a valid variable name.
160  // That matters for random=, e.g. $x..$y
161  var_end = std::adjacent_find(var_name_begin, var_end, two_dots);
162  // the default value is specified after ''?'
163  const std::string::iterator default_start = var_end < res.end() && *var_end == '?' ? var_end + 1 : res.end();
164 
165  // If the last character is '.', then it can't be a sub-variable.
166  // It's probably meant to be a period instead. Don't include it.
167  // Would need to do it repetitively if there are multiple '.'s at the end,
168  // but don't actually need to do so because the previous check for adjacent '.'s would catch that.
169  // For example, "My score is $score." or "My score is $score..."
170  if(*(var_end-1) == '.'
171  // However, "$array[$i]" by itself does not name a variable,
172  // so if "$array[$i]." is encountered, then best to include the '.',
173  // so that it more closely follows the syntax of a variable (if only to get rid of all of it).
174  // (If it's the script writer's error, they'll have to fix it in either case.)
175  // For example in "$array[$i].$field_name", if field_name does not exist as a variable,
176  // then the result of the expansion should be "", not "." (which it would be if this exception did not exist).
177  && *(var_end-2) != ']') {
178  --var_end;
179  }
180 
181  const std::string var_name(var_name_begin, var_end);
182  if(default_start == res.end()) {
183  if(var_end != res.end() && *var_end == '|') {
184  // It's been used to end this variable name; now it has no more effect.
185  // This can allow use of things like "$$composite_var_name|.x"
186  // (Yes, that's a WML 'pointer' of sorts. They are sometimes useful.)
187  // If there should still be a '|' there afterwards to affect other variable names (unlikely),
188  // just put another '|' there, one matching each '$', e.g. "$$var_containing_var_name||blah"
189  ++var_end;
190  }
191 
192 
193  if (var_name.empty()) {
194  // Allow for a way to have $s in a string.
195  // $| will be replaced by $.
196  res.replace(var_begin, var_end, "$");
197  }
198  else {
199  // The variable is replaced with its value.
200  res.replace(var_begin, var_end,
201  set.get_variable_const(var_name));
202  }
203  }
204  else {
205  var_end = default_start;
206  while(var_end != res.end() && *var_end != '|') {
207  ++var_end;
208  }
209  const std::string::iterator default_end = var_end;
210  const config::attribute_value& val = set.get_variable_const(var_name);
211  if(var_end == res.end()) {
212  res.replace(var_begin, default_start - 1, val);
213  }
214  else if(!val.empty()) {
215  res.replace(var_begin, var_end + 1, val);
216  }
217  else {
218  res.replace(var_begin, var_end + 1, std::string(default_start, default_end));
219  }
220  }
221  }
222 
223  return res;
224 }
225 
226 namespace utils {
227 
228 std::string interpolate_variables_into_string(const std::string &str, const string_map * const symbols)
229 {
230  auto set = string_map_variable_set<t_string>(*symbols);
231  return do_interpolation(str, set);
232 }
233 
234 std::string interpolate_variables_into_string(const std::string &str, const std::map<std::string,std::string> * const symbols)
235 {
236  auto set = string_map_variable_set<std::string>(*symbols);
237  return do_interpolation(str, set);
238 }
239 
240 std::string interpolate_variables_into_string(const std::string &str, const variable_set& variables)
241 {
242  return do_interpolation(str, variables);
243 }
244 
246 {
247  if(!tstr.str().empty()) {
248  std::string interp = utils::interpolate_variables_into_string(tstr.str(), variables);
249  if(tstr.str() != interp) {
250  return t_string(interp);
251  }
252  }
253  return tstr;
254 }
255 
256 std::string format_conjunct_list(const t_string& empty, const std::vector<t_string>& elems) {
257  switch(elems.size()) {
258  case 0: return empty;
259  case 1: return elems[0];
260  // TRANSLATORS: Formats a two-element conjunctive list.
261  case 2: return VGETTEXT("conjunct pair^$first and $second", {{"first", elems[0]}, {"second", elems[1]}});
262  }
263  // TRANSLATORS: Formats the first two elements of a conjunctive list.
264  std::string prefix = VGETTEXT("conjunct start^$first, $second", {{"first", elems[0]}, {"second", elems[1]}});
265  // For size=3 this loop is not entered
266  for(std::size_t i = 2; i < elems.size() - 1; i++) {
267  // TRANSLATORS: Formats successive elements of a conjunctive list.
268  prefix = VGETTEXT("conjunct mid^$prefix, $next", {{"prefix", prefix}, {"next", elems[i]}});
269  }
270  // TRANSLATORS: Formats the final element of a conjunctive list.
271  return VGETTEXT("conjunct end^$prefix, and $last", {{"prefix", prefix}, {"last", elems.back()}});
272 }
273 
274 std::string format_disjunct_list(const t_string& empty, const std::vector<t_string>& elems) {
275  switch(elems.size()) {
276  case 0: return empty;
277  case 1: return elems[0];
278  // TRANSLATORS: Formats a two-element disjunctive list.
279  case 2: return VGETTEXT("disjunct pair^$first or $second", {{"first", elems[0]}, {"second", elems[1]}});
280  }
281  // TRANSLATORS: Formats the first two elements of a disjunctive list.
282  std::string prefix = VGETTEXT("disjunct start^$first, $second", {{"first", elems[0]}, {"second", elems[1]}});
283  // For size=3 this loop is not entered
284  for(std::size_t i = 2; i < elems.size() - 1; i++) {
285  // TRANSLATORS: Formats successive elements of a disjunctive list.
286  prefix = VGETTEXT("disjunct mid^$prefix, $next", {{"prefix", prefix}, {"next", elems[i]}});
287  }
288  // TRANSLATORS: Formats the final element of a disjunctive list.
289  return VGETTEXT("disjunct end^$prefix, or $last", {{"prefix", prefix}, {"last", elems.back()}});
290 }
291 
292 std::string format_timespan(std::time_t time, bool detailed)
293 {
294  if(time <= 0) {
295  return _("timespan^expired");
296  }
297 
298  typedef std::tuple<std::time_t, const char*, const char*> time_factor;
299 
300  static const std::vector<time_factor> TIME_FACTORS{
301  // TRANSLATORS: The "timespan^$num xxxxx" strings originating from the same file
302  // as the string with this comment MUST be translated following the usual rules
303  // for WML variable interpolation -- that is, without including or translating
304  // the caret^ prefix, and leaving the $num variable specification intact, since
305  // it is technically code. The only translatable natural word to be found here
306  // is the time unit (year, month, etc.) For example, for French you would
307  // translate "timespan^$num years" as "$num ans", thus allowing the game UI to
308  // generate output such as "39 ans" after variable interpolation.
309  time_factor{ 31104000, N_n("timespan^$num year", "timespan^$num years") }, // 12 months
310  time_factor{ 2592000, N_n("timespan^$num month", "timespan^$num months") }, // 30 days
311  time_factor{ 604800, N_n("timespan^$num week", "timespan^$num weeks") },
312  time_factor{ 86400, N_n("timespan^$num day", "timespan^$num days") },
313  time_factor{ 3600, N_n("timespan^$num hour", "timespan^$num hours") },
314  time_factor{ 60, N_n("timespan^$num minute", "timespan^$num minutes") },
315  time_factor{ 1, N_n("timespan^$num second", "timespan^$num seconds") },
316  };
317 
318  std::vector<t_string> display_text;
319  string_map i18n;
320 
321  for(const auto& factor : TIME_FACTORS) {
322  const auto [ secs, fmt_singular, fmt_plural ] = factor;
323  const int amount = time / secs;
324 
325  if(amount) {
326  time -= secs * amount;
327  i18n["num"] = std::to_string(amount);
328  display_text.emplace_back(VNGETTEXT(fmt_singular, fmt_plural, amount, i18n));
329  if(!detailed) {
330  break;
331  }
332  }
333  }
334 
335  return format_conjunct_list(_("timespan^expired"), display_text);
336 }
337 
338 }
339 
340 std::string vgettext_impl(const char *domain
341  , const char *msgid
342  , const utils::string_map& symbols)
343 {
344  const std::string orig(translation::dsgettext(domain, msgid));
345  const std::string msg = utils::interpolate_variables_into_string(orig, &symbols);
346  return msg;
347 }
348 
349 std::string vngettext_impl(const char* domain,
350  const char* singular,
351  const char* plural,
352  int count,
353  const utils::string_map& symbols)
354 {
355  const std::string orig(translation::dsngettext(domain, singular, plural, count));
356  const std::string msg = utils::interpolate_variables_into_string(orig, &symbols);
357  return msg;
358 }
bool empty() const
Tests for an attribute that either was never set or was set to "".
std::string format_conjunct_list(const t_string &empty, const std::vector< t_string > &elems)
Format a conjunctive list.
#define ERR_NG
std::string interpolate_variables_into_string(const std::string &str, const string_map *const symbols)
Function which will interpolate variables, starting with &#39;$&#39; in the string &#39;str&#39; with the equivalent ...
std::map< std::string, t_string > string_map
Variant for storing WML attributes.
#define a
std::string dsngettext(const char *domainname, const char *singular, const char *plural, int n)
Definition: gettext.cpp:456
std::shared_ptr< config > temp_
std::string format_disjunct_list(const t_string &empty, const std::vector< t_string > &elems)
Format a disjunctive list.
#define VNGETTEXT(msgid, msgid_plural, count,...)
static void msg(const char *act, debug_info &i, const char *to="", const char *result="")
Definition: debugger.cpp:110
#define WRN_NG
static std::string _(const char *str)
Definition: gettext.hpp:93
Definitions for the interface to Wesnoth Markup Language (WML).
const std::map< std::string, T > & map_
std::string dsgettext(const char *domainname, const char *msgid)
Definition: gettext.cpp:426
#define b
variable_info< const variable_info_implementation::vi_policy_const > variable_access_const
Read-only access.
virtual config::attribute_value get_variable_const(const std::string &key) const
static std::string do_interpolation(const std::string &str, const variable_set &set)
virtual variable_access_const get_variable_access_read(const std::string &varname) const
#define N_n(String1, String2)
Definition: gettext.hpp:102
string_map_variable_set(const std::map< std::string, T > &map)
std::string format_timespan(std::time_t time, bool detailed)
Formats a timespan into human-readable text for player authentication functions.
std::string(* evaluate_formula)(const std::string &formula)
static bool two_dots(char a, char b)
std::size_t i
Definition: function.cpp:967
mock_party p
#define VGETTEXT(msgid,...)
Handy wrappers around interpolate_variables_into_string and gettext.
static lg::log_domain log_engine("engine")
Information on a WML variable.
Standard logging facilities (interface).
std::string vngettext_impl(const char *domain, const char *singular, const char *plural, int count, const utils::string_map &symbols)
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:61
mock_char c
const std::string & str() const
Definition: tstring.hpp:191
t_string interpolate_variables_into_tstring(const t_string &tstr, const variable_set &variables)
Function that does the same as the above, for t_stringS.
std::string::const_iterator iterator
Definition: tokenizer.hpp:25
std::string vgettext_impl(const char *domain, const char *msgid, const utils::string_map &symbols)
Implementation functions for the VGETTEXT and VNGETTEXT macros.