The Battle for Wesnoth  1.19.27+dev
lua_stringx.cpp
Go to the documentation of this file.
1 /*
2  Copyright (C) 2014 - 2025
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 "utils/span.hpp"
23 #include "variable.hpp" // for config_variable_set
24 
25 #include <boost/algorithm/string/trim.hpp>
26 #include <cstring>
27 
28 
29 namespace lua_stringx {
30 
31 /**
32 * Formats a message by interpolating WML variable syntax
33 * Arg 1: (optional) Logger
34 * Arg 2: Message
35 */
36 static int intf_format(lua_State* L)
37 {
38  config cfg = luaW_checkconfig(L, 2);
39  config_variable_set variables(cfg);
40  if(lua_isstring(L, 1)) {
41  std::string str = lua_tostring(L, 1);
43  return 1;
44  }
45  t_string str = luaW_checktstring(L, 1);
47  return 1;
48 }
49 
50 /**
51 * Formats a list into human-readable format
52 * Arg 1: default value, used if the list is empty
53 * Arg 2: list of strings
54 */
55 template<bool conjunct>
56 static int intf_format_list(lua_State* L)
57 {
58  const t_string empty = luaW_checktstring(L, 1);
59  auto values = lua_check<std::vector<t_string>>(L, 2);
61  return 1;
62 }
63 
64 /**
65 * Enables indexing a string by an integer, while also treating the stringx module as its metatable.__index
66 */
67 static int impl_str_index(lua_State* L)
68 {
69  if(lua_type(L, 2) == LUA_TSTRING) {
70  // return stringx[key]
71  lua_getglobal(L, "stringx");
72  lua_pushvalue(L, 2);
73  lua_gettable(L, -2);
74  return 1;
75  } else if(lua_type(L, 2) == LUA_TNUMBER) {
76  // get the string length and the index
77  int len = lua_rawlen(L, 1);
78  int i = luaL_checkinteger(L, 2);
79  // In order to not break ipairs, an out-of-bounds access needs to return nil
80  if(i == 0 || abs(i) > len) {
81  lua_pushnil(L);
82  return 1;
83  }
84  // return string.sub(str, key, key)
85  luaW_getglobal(L, "string", "sub");
86  lua_pushvalue(L, 1);
87  lua_pushvalue(L, 2);
88  lua_pushvalue(L, 2);
89  lua_call(L, 3, 1);
90  return 1;
91  }
92  return 0;
93 }
94 
95 /**
96 * Splits a string into parts according to options
97 * Arg 1: String to split
98 * Arg 2: Separator
99 * Arg 3: Options table
100 */
101 static int intf_str_split(lua_State* L)
102 {
103  enum {BASIC, ESCAPED, PAREN, ANIM} type = BASIC;
104  const std::string& str = luaL_checkstring(L, 1);
105  const std::string& sep = luaL_optstring(L, 2, ",");
106  std::string left, right;
108  if(lua_istable(L, 3)) {
109  flags = 0;
110  if(luaW_table_get_def(L, 3, "remove_empty", true)) {
111  flags |= utils::REMOVE_EMPTY;
112  }
113  if(luaW_table_get_def(L, 3, "strip_spaces", true)) {
114  flags |= utils::STRIP_SPACES;
115  }
116  bool anim = luaW_table_get_def(L, 3, "expand_anim", false);
117  if(luaW_tableget(L, 3, "escape")) {
118  if(anim) {
119  return luaL_error(L, "escape and expand_anim options are incompatible!");
120  }
121  type = ESCAPED;
122  left = luaL_checkstring(L, -1);
123  if(left.size() != 1) {
124  return luaL_error(L, "escape must be a single character");
125  }
126  } else if(luaW_tableget(L, 3, "quote")) {
127  left = right = luaL_checkstring(L, -1);
128  if(anim) {
129  type = ANIM;
130  left.push_back('[');
131  right.push_back(']');
132  } else type = PAREN;
133  } else if(luaW_tableget(L, 3, "quote_left") && luaW_tableget(L, 3, "quote_right")) {
134  left = luaL_checkstring(L, -2);
135  right = luaL_checkstring(L, -1);
136  if(anim) {
137  if(left.find_first_of("[]") != std::string::npos || right.find_first_of("[]") != std::string::npos) {
138  return luaL_error(L, "left and right cannot include square brackets [] if expand_anim is enabled");
139  }
140  type = ANIM;
141  left.push_back('[');
142  right.push_back(']');
143  } else type = PAREN;
144  } else if(anim) {
145  type = ANIM;
146  left = "([";
147  right = ")]";
148  }
149  if(type != ESCAPED && left.size() != right.size()) {
150  return luaL_error(L, "left and right need to be strings of the same length");
151  }
152  }
153  switch(type) {
154  case BASIC:
155  lua_push(L, utils::split(str, sep[0], flags));
156  break;
157  case ESCAPED:
158  lua_push(L, utils::quoted_split(str, sep[0], flags, left[0]));
159  break;
160  case PAREN:
161  lua_push(L, utils::parenthetical_split(str, sep[0], left, right, flags));
162  break;
163  case ANIM:
164  lua_push(L, utils::square_parenthetical_split(str, sep[0], left, right, flags));
165  break;
166  }
167  return 1;
168 }
169 
170 /**
171 * Splits a string into parenthesized portions and portions between parenthesized portions
172 * Arg 1: String to split
173 * Arg 2: Possible left parentheses
174 * Arg 3: Matching right parentheses
175 */
176 static int intf_str_paren_split(lua_State* L)
177 {
178  const std::string& str = luaL_checkstring(L, 1);
179  const std::string& left = luaL_optstring(L, 2, "(");
180  const std::string& right = luaL_optstring(L, 3, ")");
181  if(left.size() != right.size()) {
182  return luaL_error(L, "left and right need to be strings of the same length");
183  }
184  bool strip_spaces = luaL_opt(L, luaW_toboolean, 4, true);
185  lua_push(L, utils::parenthetical_split(str, 0, left, right, strip_spaces ? utils::STRIP_SPACES : 0));
186  return 1;
187 }
188 
189 /**
190 * Splits a string into a map
191 * Arg 1: string to split
192 * Arg 2: Separator for items
193 * Arg 3: Separator for key and value
194 */
195 static int intf_str_map_split(lua_State* L)
196 {
197  const std::string& str = luaL_checkstring(L, 1);
198  const std::string& sep = luaL_optstring(L, 2, ",");
199  const std::string& kv = luaL_optstring(L, 3, ":");
200  std::string dflt;
201  if(sep.size() != 1) {
202  return luaL_error(L, "separator must be a single character");
203  }
204  if(kv.size() != 1) {
205  return luaL_error(L, "key_value_separator must be a single character");
206  }
208  if(lua_istable(L, 4)) {
209  flags = 0;
210  if(luaW_table_get_def(L, 4, "remove_empty", true)) {
211  flags |= utils::REMOVE_EMPTY;
212  }
213  if(luaW_table_get_def(L, 4, "strip_spaces", true)) {
214  flags |= utils::STRIP_SPACES;
215  }
216  if(luaW_tableget(L, 4, "default")) {
217  dflt = luaL_checkstring(L, -1);
218  }
219  }
220  lua_push(L, utils::map_split(str, sep[0], kv[0], flags, dflt));
221  return 1;
222 }
223 
224 /**
225 * Joins a list into a string; calls __tostring and __index metamethods
226 * Arg 1: list to join
227 * Arg 2: separator
228 * (arguments can be swapped)
229 */
230 static int intf_str_join(lua_State* L) {
231  // Support both join(list, [sep]) and join(sep, list)
232  // The latter form means sep:join(list) also works.
233  std::string sep;
234  int list_idx;
235  if(lua_istable(L, 1)) {
236  list_idx = 1;
237  sep = luaL_optstring(L, 2, ",");
238  } else if(lua_istable(L, 2)) {
239  sep = luaL_checkstring(L, 1);
240  list_idx = 2;
241  } else return luaL_error(L, "invalid arguments to join, should have map and separator");
242  std::vector<std::string> pieces;
243  for(int i = 1; i <= luaL_len(L, list_idx); i++) {
244  lua_getglobal(L, "tostring");
245  lua_geti(L, list_idx, i);
246  lua_call(L, 1, 1);
247  pieces.push_back(luaL_checkstring(L, -1));
248  }
249  lua_push(L, utils::join(pieces, sep));
250  return 1;
251 }
252 
253 /**
254 * Joins a map into a string; calls __tostring metamethods (on both key and value) but not __index
255 * Arg 1: list to join
256 * Arg 2: separator for items
257 * Arg 3: separator for key and value
258 * (list argument can be swapped to any position)
259 */
260 static int intf_str_join_map(lua_State* L) {
261  // Support join_map(map, [sep], [kv_sep]), join_map(sep, map, [kv_sep]), and join_map(sep, kv_sep, map)
262  // The latter forms mean sep:join_map(kv_sep, map) and sep:join_map(map) also work.
263  // If only one separator is given in the first form, it will be sep, not kv_sep
264  std::string sep, kv;
265  int map_idx;
266  if(lua_istable(L, 1)) {
267  map_idx = 1;
268  sep = luaL_optstring(L, 2, ",");
269  kv = luaL_optstring(L, 3, ":");
270  } else if(lua_istable(L, 2)) {
271  sep = luaL_checkstring(L, 1);
272  map_idx = 2;
273  kv = luaL_optstring(L, 3, ":");
274  } else if(lua_istable(L, 3)) {
275  sep = luaL_checkstring(L, 1);
276  kv = luaL_checkstring(L, 2);
277  map_idx = 3;
278  } else return luaL_error(L, "invalid arguments to join_map, should have map, separator, and key_value_separator");
279  std::map<std::string, std::string> pieces;
280  for(lua_pushnil(L); lua_next(L, map_idx); /*pop in loop body*/) {
281  int key_idx = lua_absindex(L, -2), val_idx = lua_absindex(L, -1);
282  lua_getglobal(L, "tostring");
283  lua_pushvalue(L, key_idx);
284  lua_call(L, 1, 1);
285  std::string& val = pieces[luaL_checkstring(L, -1)];
286  lua_getglobal(L, "tostring");
287  lua_pushvalue(L, val_idx);
288  lua_call(L, 1, 1);
289  val = luaL_checkstring(L, -1);
290  lua_settop(L, key_idx);
291  }
292  lua_push(L, utils::join_map(pieces, sep, kv));
293  return 1;
294 }
295 
296 /**
297  * Trims whitespace from the beginning and end of a string
298  */
299 static int intf_str_trim(lua_State* L)
300 {
301  std::string str = luaL_checkstring(L, 1);
302  boost::trim(str);
303  lua_pushlstring(L, str.c_str(), str.size());
304  return 1;
305 }
306 
307 // Override string.format to coerce the format to a string
308 // Tables and userdata without __tostring are converted before delegation so
309 // luaL_tolstring cannot include memory addresses in formatted output.
310 static int intf_str_format(lua_State* L)
311 {
312  int nargs = lua_gettop(L);
313  if(luaW_iststring(L, 1)) {
314  // get the tostring() function and call it on the first argument
315  lua_getglobal(L, "tostring");
316  lua_pushvalue(L, 1);
317  lua_call(L, 1, 1);
318  // replace the first argument with the coerced value
319  lua_replace(L, 1);
320  }
321 
322  for(int argument = 2; argument <= nargs; ++argument) {
323  const int type = lua_type(L, argument);
324  if(type != LUA_TTABLE && type != LUA_TUSERDATA) {
325  continue;
326  }
327 
328  const int tostring_type = luaL_getmetafield(L, argument, "__tostring");
329  if(tostring_type != LUA_TNIL) {
330  lua_pop(L, 1);
331  continue;
332  }
333 
334  lua_getglobal(L, "tostring");
335  lua_pushvalue(L, argument);
336  lua_call(L, 1, 1);
337  lua_replace(L, argument);
338  }
339  // raise an error if the string contains a %p specifier
340  // Lua's formatter reads bytes after an embedded NUL, so inspect the full Lua string.
341  bool in_specifier = false;
342  std::size_t format_length = 0;
343  const char* str = luaL_checklstring(L, 1, &format_length);
344  for(char c : utils::span<const char>(str, format_length)) {
345  if(c == '%') {
346  in_specifier = !in_specifier;
347  } else if(in_specifier) {
348  if(c == 'p') {
349  lua_pushstring(L, "%p format specifier is not supported");
350  return lua_error(L);
351  }
352  // strchr cannot test NUL as an ordinary character because its character list
353  // is NUL-terminated, so check it separately.
354  if(c == '\0' || std::strchr("-+#0 123456789.", c) == nullptr) {
355  in_specifier = false;
356  }
357  }
358  }
359  // grab the original string.format function from the closure...
360  lua_pushvalue(L, lua_upvalueindex(1));
361  // ...move it to the bottom of the stack...
362  lua_insert(L, 1);
363  // ...and finally pass along all the arguments to it.
364  lua_call(L, nargs, 1);
365  return 1;
366 }
367 
368 /**
369  * Parses a range string of the form a-b into an interval pair
370  * Accepts the string "infinity" as representing a Very Large Number
371  * Arg 2: (optional) If true, parse as real numbers instead of integers
372  */
373 static int intf_parse_range(lua_State* L)
374 {
375  const std::string str = luaL_checkstring(L, 1);
376  if(luaL_opt(L, lua_toboolean, 2, false)) {
377  auto interval = utils::parse_range_real(str);
378  lua_pushnumber(L, interval.first);
379  lua_pushnumber(L, interval.second);
380  } else {
381  auto interval = utils::parse_range(str);
382  lua_pushinteger(L, interval.first);
383  lua_pushinteger(L, interval.second);
384  }
385  return 2;
386 }
387 
388 int luaW_open(lua_State* L) {
389  auto& lk = lua_kernel_base::get_lua_kernel<lua_kernel_base>(L);
390  lk.add_log("Adding stringx module...\n");
391  static luaL_Reg const str_callbacks[] = {
392  { "split", &intf_str_split },
393  { "parenthetical_split", &intf_str_paren_split },
394  { "map_split", &intf_str_map_split },
395  { "join", &intf_str_join },
396  { "join_map", &intf_str_join_map },
397  { "trim", &intf_str_trim },
398  { "parse_range", &intf_parse_range },
399  { "vformat", &intf_format },
400  { "format_conjunct_list", &intf_format_list<true> },
401  { "format_disjunct_list", &intf_format_list<false> },
402  { nullptr, nullptr },
403  };
404  lua_newtable(L);
405  luaL_setfuncs(L, str_callbacks, 0);
406  // Set the stringx metatable to index the string module
407  lua_createtable(L, 0, 1);
408  lua_getglobal(L, "string");
409  lua_setfield(L, -2, "__index");
410  lua_setmetatable(L, -2);
411 
412  // Set the metatable of strings to index the stringx module instead of the string module
413  lua_pushliteral(L, "");
414  lua_getmetatable(L, -1);
415  lua_pushcfunction(L, &impl_str_index);
416  lua_setfield(L, -2, "__index");
417  lua_setmetatable(L, -2);
418  lua_pop(L, 1);
419 
420  // Override string.format so it can accept a t_string
421  lua_getglobal(L, "string");
422  lua_getfield(L, -1, "format");
423  lua_pushcclosure(L, &intf_str_format, 1);
424  lua_setfield(L, -2, "format");
425  lua_pop(L, 1);
426  return 1;
427 }
428 
429 }
A config object defines a single node in a WML file, with access to child nodes.
Definition: config.hpp:157
const config * cfg
std::size_t i
Definition: function.cpp:1031
bool luaW_iststring(lua_State *L, int index)
Definition: lua_common.cpp:605
config luaW_checkconfig(lua_State *L, int index)
Converts an optional table or vconfig to a config object.
Definition: lua_common.cpp:990
bool luaW_toboolean(lua_State *L, int n)
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.
t_string luaW_checktstring(lua_State *L, int index)
Converts a scalar to a translatable string.
Definition: lua_common.cpp:597
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...
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:36
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:56
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:67
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)
constexpr auto values
Definition: ranges.hpp:46
@ 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 join(const Range &v, const std::string &s=",")
Generates a new string joining container items in a list.
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 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
mock_char c