The Battle for Wesnoth  1.19.7+dev
lua_stringx.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2014 - 2024
3  by Chris Beck <render787@gmail.com>
4  Part of the Battle for Wesnoth Project https://www.wesnoth.org/
5 
6  This program is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10  This program is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY.
12 
13  See the COPYING file for more details.
14 */
15 
18 #include "scripting/lua_common.hpp"
19 #include "scripting/push_check.hpp"
20 
21 #include "formula/string_utils.hpp"
22 #include "variable.hpp" // for config_variable_set
23 
24 #include <boost/algorithm/string/trim.hpp>
25 
26 
27 namespace lua_stringx {
28 
29 /**
30 * Formats a message by interpolating WML variable syntax
31 * Arg 1: (optional) Logger
32 * Arg 2: Message
33 */
34 static int intf_format(lua_State* L)
35 {
36  config cfg = luaW_checkconfig(L, 2);
37  config_variable_set variables(cfg);
38  if(lua_isstring(L, 1)) {
39  std::string str = lua_tostring(L, 1);
41  return 1;
42  }
43  t_string str = luaW_checktstring(L, 1);
45  return 1;
46 }
47 
48 /**
49 * Formats a list into human-readable format
50 * Arg 1: default value, used if the list is empty
51 * Arg 2: list of strings
52 */
53 template<bool conjunct>
54 static int intf_format_list(lua_State* L)
55 {
56  const t_string empty = luaW_checktstring(L, 1);
57  auto values = lua_check<std::vector<t_string>>(L, 2);
58  lua_push(L, (conjunct ? utils::format_conjunct_list : utils::format_disjunct_list)(empty, values));
59  return 1;
60 }
61 
62 /**
63 * Enables indexing a string by an integer, while also treating the stringx module as its metatable.__index
64 */
65 static int impl_str_index(lua_State* L)
66 {
67  if(lua_type(L, 2) == LUA_TSTRING) {
68  // return stringx[key]
69  lua_getglobal(L, "stringx");
70  lua_pushvalue(L, 2);
71  lua_gettable(L, -2);
72  return 1;
73  } else if(lua_type(L, 2) == LUA_TNUMBER) {
74  // get the string length and the index
75  int len = lua_rawlen(L, 1);
76  int i = luaL_checkinteger(L, 2);
77  // In order to not break ipairs, an out-of-bounds access needs to return nil
78  if(i == 0 || abs(i) > len) {
79  lua_pushnil(L);
80  return 1;
81  }
82  // return string.sub(str, key, key)
83  luaW_getglobal(L, "string", "sub");
84  lua_pushvalue(L, 1);
85  lua_pushvalue(L, 2);
86  lua_pushvalue(L, 2);
87  lua_call(L, 3, 1);
88  return 1;
89  }
90  return 0;
91 }
92 
93 /**
94 * Splits a string into parts according to options
95 * Arg 1: String to split
96 * Arg 2: Separator
97 * Arg 3: Options table
98 */
99 static int intf_str_split(lua_State* L)
100 {
101  enum {BASIC, ESCAPED, PAREN, ANIM} type = BASIC;
102  const std::string& str = luaL_checkstring(L, 1);
103  const std::string& sep = luaL_optstring(L, 2, ",");
104  std::string left, right;
106  if(lua_istable(L, 3)) {
107  flags = 0;
108  if(luaW_table_get_def(L, 3, "remove_empty", true)) {
109  flags |= utils::REMOVE_EMPTY;
110  }
111  if(luaW_table_get_def(L, 3, "strip_spaces", true)) {
112  flags |= utils::STRIP_SPACES;
113  }
114  bool anim = luaW_table_get_def(L, 3, "expand_anim", false);
115  if(luaW_tableget(L, 3, "escape")) {
116  if(anim) {
117  return luaL_error(L, "escape and expand_anim options are incompatible!");
118  }
119  type = ESCAPED;
120  left = luaL_checkstring(L, -1);
121  if(left.size() != 1) {
122  return luaL_error(L, "escape must be a single character");
123  }
124  } else if(luaW_tableget(L, 3, "quote")) {
125  left = right = luaL_checkstring(L, -1);
126  if(anim) {
127  type = ANIM;
128  left.push_back('[');
129  right.push_back(']');
130  } else type = PAREN;
131  } else if(luaW_tableget(L, 3, "quote_left") && luaW_tableget(L, 3, "quote_right")) {
132  left = luaL_checkstring(L, -2);
133  right = luaL_checkstring(L, -1);
134  if(anim) {
135  if(left.find_first_of("[]") != std::string::npos || right.find_first_of("[]") != std::string::npos) {
136  return luaL_error(L, "left and right cannot include square brackets [] if expand_anim is enabled");
137  }
138  type = ANIM;
139  left.push_back('[');
140  right.push_back(']');
141  } else type = PAREN;
142  } else if(anim) {
143  type = ANIM;
144  left = "([";
145  right = ")]";
146  }
147  if(type != ESCAPED && left.size() != right.size()) {
148  return luaL_error(L, "left and right need to be strings of the same length");
149  }
150  }
151  switch(type) {
152  case BASIC:
153  lua_push(L, utils::split(str, sep[0], flags));
154  break;
155  case ESCAPED:
156  lua_push(L, utils::quoted_split(str, sep[0], flags, left[0]));
157  break;
158  case PAREN:
159  lua_push(L, utils::parenthetical_split(str, sep[0], left, right, flags));
160  break;
161  case ANIM:
162  lua_push(L, utils::square_parenthetical_split(str, sep[0], left, right, flags));
163  break;
164  }
165  return 1;
166 }
167 
168 /**
169 * Splits a string into parenthesized portions and portions between parenthesized portions
170 * Arg 1: String to split
171 * Arg 2: Possible left parentheses
172 * Arg 3: Matching right parentheses
173 */
174 static int intf_str_paren_split(lua_State* L)
175 {
176  const std::string& str = luaL_checkstring(L, 1);
177  const std::string& left = luaL_optstring(L, 2, "(");
178  const std::string& right = luaL_optstring(L, 3, ")");
179  if(left.size() != right.size()) {
180  return luaL_error(L, "left and right need to be strings of the same length");
181  }
182  bool strip_spaces = luaL_opt(L, luaW_toboolean, 4, true);
183  lua_push(L, utils::parenthetical_split(str, 0, left, right, strip_spaces ? utils::STRIP_SPACES : 0));
184  return 1;
185 }
186 
187 /**
188 * Splits a string into a map
189 * Arg 1: string to split
190 * Arg 2: Separator for items
191 * Arg 3: Separator for key and value
192 */
193 static int intf_str_map_split(lua_State* L)
194 {
195  const std::string& str = luaL_checkstring(L, 1);
196  const std::string& sep = luaL_optstring(L, 2, ",");
197  const std::string& kv = luaL_optstring(L, 3, ":");
198  std::string dflt;
199  if(sep.size() != 1) {
200  return luaL_error(L, "separator must be a single character");
201  }
202  if(kv.size() != 1) {
203  return luaL_error(L, "key_value_separator must be a single character");
204  }
206  if(lua_istable(L, 4)) {
207  flags = 0;
208  if(luaW_table_get_def(L, 4, "remove_empty", true)) {
209  flags |= utils::REMOVE_EMPTY;
210  }
211  if(luaW_table_get_def(L, 4, "strip_spaces", true)) {
212  flags |= utils::STRIP_SPACES;
213  }
214  if(luaW_tableget(L, 4, "default")) {
215  dflt = luaL_checkstring(L, -1);
216  }
217  }
218  lua_push(L, utils::map_split(str, sep[0], kv[0], flags, dflt));
219  return 1;
220 }
221 
222 /**
223 * Joins a list into a string; calls __tostring and __index metamethods
224 * Arg 1: list to join
225 * Arg 2: separator
226 * (arguments can be swapped)
227 */
228 static int intf_str_join(lua_State* L) {
229  // Support both join(list, [sep]) and join(sep, list)
230  // The latter form means sep:join(list) also works.
231  std::string sep;
232  int list_idx;
233  if(lua_istable(L, 1)) {
234  list_idx = 1;
235  sep = luaL_optstring(L, 2, ",");
236  } else if(lua_istable(L, 2)) {
237  sep = luaL_checkstring(L, 1);
238  list_idx = 2;
239  } else return luaL_error(L, "invalid arguments to join, should have map and separator");
240  std::vector<std::string> pieces;
241  for(int i = 1; i <= luaL_len(L, list_idx); i++) {
242  lua_getglobal(L, "tostring");
243  lua_geti(L, list_idx, i);
244  lua_call(L, 1, 1);
245  pieces.push_back(luaL_checkstring(L, -1));
246  }
247  lua_push(L, utils::join(pieces, sep));
248  return 1;
249 }
250 
251 /**
252 * Joins a map into a string; calls __tostring metamethods (on both key and value) but not __index
253 * Arg 1: list to join
254 * Arg 2: separator for items
255 * Arg 3: separator for key and value
256 * (list argument can be swapped to any position)
257 */
258 static int intf_str_join_map(lua_State* L) {
259  // Support join_map(map, [sep], [kv_sep]), join_map(sep, map, [kv_sep]), and join_map(sep, kv_sep, map)
260  // The latter forms mean sep:join_map(kv_sep, map) and sep:join_map(map) also work.
261  // If only one separator is given in the first form, it will be sep, not kv_sep
262  std::string sep, kv;
263  int map_idx;
264  if(lua_istable(L, 1)) {
265  map_idx = 1;
266  sep = luaL_optstring(L, 2, ",");
267  kv = luaL_optstring(L, 3, ":");
268  } else if(lua_istable(L, 2)) {
269  sep = luaL_checkstring(L, 1);
270  map_idx = 2;
271  kv = luaL_optstring(L, 3, ":");
272  } else if(lua_istable(L, 3)) {
273  sep = luaL_checkstring(L, 1);
274  kv = luaL_checkstring(L, 2);
275  map_idx = 3;
276  } else return luaL_error(L, "invalid arguments to join_map, should have map, separator, and key_value_separator");
277  std::map<std::string, std::string> pieces;
278  for(lua_pushnil(L); lua_next(L, map_idx); /*pop in loop body*/) {
279  int key_idx = lua_absindex(L, -2), val_idx = lua_absindex(L, -1);
280  lua_getglobal(L, "tostring");
281  lua_pushvalue(L, key_idx);
282  lua_call(L, 1, 1);
283  std::string& val = pieces[luaL_checkstring(L, -1)];
284  lua_getglobal(L, "tostring");
285  lua_pushvalue(L, val_idx);
286  lua_call(L, 1, 1);
287  val = luaL_checkstring(L, -1);
288  lua_settop(L, key_idx);
289  }
290  lua_push(L, utils::join_map(pieces, sep, kv));
291  return 1;
292 }
293 
294 /**
295  * Trims whitespace from the beginning and end of a string
296  */
297 static int intf_str_trim(lua_State* L)
298 {
299  std::string str = luaL_checkstring(L, 1);
300  boost::trim(str);
301  lua_pushlstring(L, str.c_str(), str.size());
302  return 1;
303 }
304 
305 // Override string.format to coerce the format to a string
306 static int intf_str_format(lua_State* L)
307 {
308  int nargs = lua_gettop(L);
309  if(luaW_iststring(L, 1)) {
310  // get the tostring() function and call it on the first argument
311  lua_getglobal(L, "tostring");
312  lua_pushvalue(L, 1);
313  lua_call(L, 1, 1);
314  // replace the first argument with the coerced value
315  lua_replace(L, 1);
316  }
317  // grab the original string.format function from the closure...
318  lua_pushvalue(L, lua_upvalueindex(1));
319  // ...move it to the bottom of the stack...
320  lua_insert(L, 1);
321  // ...and finally pass along all the arguments to it.
322  lua_call(L, nargs, 1);
323  return 1;
324 }
325 
326 /**
327  * Parses a range string of the form a-b into an interval pair
328  * Accepts the string "infinity" as representing a Very Large Number
329  * Arg 2: (optional) If true, parse as real numbers instead of integers
330  */
331 static int intf_parse_range(lua_State* L)
332 {
333  const std::string str = luaL_checkstring(L, 1);
334  if(luaL_opt(L, lua_toboolean, 2, false)) {
335  auto interval = utils::parse_range_real(str);
336  lua_pushnumber(L, interval.first);
337  lua_pushnumber(L, interval.second);
338  } else {
339  auto interval = utils::parse_range(str);
340  lua_pushinteger(L, interval.first);
341  lua_pushinteger(L, interval.second);
342  }
343  return 2;
344 }
345 
346 int luaW_open(lua_State* L) {
347  auto& lk = lua_kernel_base::get_lua_kernel<lua_kernel_base>(L);
348  lk.add_log("Adding stringx module...\n");
349  static luaL_Reg const str_callbacks[] = {
350  { "split", &intf_str_split },
351  { "parenthetical_split", &intf_str_paren_split },
352  { "map_split", &intf_str_map_split },
353  { "join", &intf_str_join },
354  { "join_map", &intf_str_join_map },
355  { "trim", &intf_str_trim },
356  { "parse_range", &intf_parse_range },
357  { "vformat", &intf_format },
358  { "format_conjunct_list", &intf_format_list<true> },
359  { "format_disjunct_list", &intf_format_list<false> },
360  { nullptr, nullptr },
361  };
362  lua_newtable(L);
363  luaL_setfuncs(L, str_callbacks, 0);
364  // Set the stringx metatable to index the string module
365  lua_createtable(L, 0, 1);
366  lua_getglobal(L, "string");
367  lua_setfield(L, -2, "__index");
368  lua_setmetatable(L, -2);
369 
370  // Set the metatable of strings to index the stringx module instead of the string module
371  lua_pushliteral(L, "");
372  lua_getmetatable(L, -1);
373  lua_pushcfunction(L, &impl_str_index);
374  lua_setfield(L, -2, "__index");
375  lua_setmetatable(L, -2);
376  lua_pop(L, 1);
377 
378  // Override string.format so it can accept a t_string
379  lua_getglobal(L, "string");
380  lua_getfield(L, -1, "format");
381  lua_pushcclosure(L, &intf_str_format, 1);
382  lua_setfield(L, -2, "format");
383  lua_pop(L, 1);
384  return 1;
385 }
386 
387 }
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:172
std::size_t i
Definition: function.cpp:1029
bool luaW_iststring(lua_State *L, int index)
Definition: lua_common.cpp:643
config luaW_checkconfig(lua_State *L, int index)
Converts an optional table or vconfig to a config object.
Definition: lua_common.cpp:927
bool luaW_toboolean(lua_State *L, int n)
Definition: lua_common.cpp:998
bool luaW_tableget(lua_State *L, int index, const char *key)
bool luaW_getglobal(lua_State *L, const std::vector< std::string > &path)
Pushes the value found by following the variadic names (char *), if the value is not nil.
Definition: lua_common.cpp:979
t_string luaW_checktstring(lua_State *L, int index)
Converts a scalar to a translatable string.
Definition: lua_common.cpp:635
static int intf_str_split(lua_State *L)
Splits a string into parts according to options Arg 1: String to split Arg 2: Separator Arg 3: Option...
Definition: lua_stringx.cpp:99
static int intf_str_map_split(lua_State *L)
Splits a string into a map Arg 1: string to split Arg 2: Separator for items Arg 3: Separator for key...
static int intf_parse_range(lua_State *L)
Parses a range string of the form a-b into an interval pair Accepts the string "infinity" as represen...
static int intf_format(lua_State *L)
Formats a message by interpolating WML variable syntax Arg 1: (optional) Logger Arg 2: Message.
Definition: lua_stringx.cpp:34
static int intf_str_paren_split(lua_State *L)
Splits a string into parenthesized portions and portions between parenthesized portions Arg 1: String...
static int intf_format_list(lua_State *L)
Formats a list into human-readable format Arg 1: default value, used if the list is empty Arg 2: list...
Definition: lua_stringx.cpp:54
static int intf_str_trim(lua_State *L)
Trims whitespace from the beginning and end of a string.
static int impl_str_index(lua_State *L)
Enables indexing a string by an integer, while also treating the stringx module as its metatable....
Definition: lua_stringx.cpp:65
static int intf_str_join(lua_State *L)
Joins a list into a string; calls __tostring and __index metamethods Arg 1: list to join Arg 2: separ...
static int intf_str_format(lua_State *L)
static int intf_str_join_map(lua_State *L)
Joins a map into a string; calls __tostring metamethods (on both key and value) but not __index Arg 1...
int luaW_open(lua_State *L)
@ STRIP_SPACES
REMOVE_EMPTY: remove empty elements.
@ REMOVE_EMPTY
void trim(std::string_view &s)
std::string interpolate_variables_into_string(const std::string &str, const string_map *const symbols)
Function which will interpolate variables, starting with '$' in the string 'str' with the equivalent ...
std::map< std::string, std::string > map_split(const std::string &val, char major, char minor, int flags, const std::string &default_value)
Splits a string based on two separators into a map.
std::vector< std::string > quoted_split(const std::string &val, char c, int flags, char quote)
This function is identical to split(), except it does not split when it otherwise would if the previo...
std::string join_map(const T &v, const std::string &major=",", const std::string &minor=":")
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::vector< std::string > parenthetical_split(std::string_view val, const char separator, std::string_view left, std::string_view right, const int flags)
Splits a string based either on a separator, except then the text appears within specified parenthesi...
std::string format_disjunct_list(const t_string &empty, const std::vector< t_string > &elems)
Format a disjunctive list.
std::vector< std::string > square_parenthetical_split(const std::string &val, const char separator, const std::string &left, const std::string &right, const int flags)
Similar to parenthetical_split, but also expands embedded square brackets.
std::pair< int, int > parse_range(std::string_view str)
Recognises the following patterns, and returns a {min, max} pair.
std::string join(const T &v, const std::string &s=",")
Generates a new string joining container items in a list.
std::string format_conjunct_list(const t_string &empty, const std::vector< t_string > &elems)
Format a conjunctive list.
std::pair< double, double > parse_range_real(std::string_view str)
Recognises similar patterns to parse_range, and returns a {min, max} pair.
std::vector< std::string > split(const config_attribute_value &val)
void lua_push(lua_State *L, const T &val)
Definition: push_check.hpp:425
std::decay_t< T > luaW_table_get_def(lua_State *L, int index, std::string_view k, const T &def)
returns t[k] where k is the table at index index and k is k or def if it is not convertible to the co...
Definition: push_check.hpp:435