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