The Battle for Wesnoth 1.21.0-dev
 
Loading...
Searching...
No Matches
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
20
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
29namespace lua_stringx {
30
31/**
32* Formats a message by interpolating WML variable syntax
33* Arg 1: (optional) Logger
34* Arg 2: Message
35*/
36static int intf_format(lua_State* L)
37{
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*/
55template<bool conjunct>
56static 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*/
67static 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*/
101static 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*/
176static 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*/
195static 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*/
230static 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*/
260static 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 */
299static 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.
310static 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 */
373static 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
388int 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
bool luaW_iststring(lua_State *L, int index)
config luaW_checkconfig(lua_State *L, int index)
Converts an optional table or vconfig to a config object.
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.
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.
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...
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....
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.
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)
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...
void lua_push(lua_State *L, const T &val)
#define i
#define c