2022-04-30 21:09:11 +02:00
|
|
|
#include "VFormat.h"
|
|
|
|
|
|
|
|
#include <SDL.h>
|
|
|
|
#include <stdbool.h>
|
|
|
|
|
2022-12-01 07:30:16 +01:00
|
|
|
#include "Alloc.h"
|
2022-04-30 21:09:11 +02:00
|
|
|
#include "CWrappers.h"
|
2023-02-23 05:14:33 +01:00
|
|
|
#include "UTF8.h"
|
2022-04-30 21:09:11 +02:00
|
|
|
|
|
|
|
|
|
|
|
static inline bool is_whitespace(char ch)
|
|
|
|
{
|
|
|
|
return ch == ' ' || ch == '\t';
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline void trim_whitespace(const char** string, size_t* bytes)
|
|
|
|
{
|
|
|
|
/* Strips leading and trailing whitespace. */
|
|
|
|
|
|
|
|
while (*bytes > 0 && is_whitespace((*string)[0]))
|
|
|
|
{
|
|
|
|
(*string)++;
|
|
|
|
(*bytes)--;
|
|
|
|
}
|
|
|
|
while (*bytes > 0 && is_whitespace((*string)[*bytes - 1]))
|
|
|
|
{
|
|
|
|
(*bytes)--;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
static inline void call_with_button(format_callback callback, void* userdata, int button_value)
|
|
|
|
{
|
|
|
|
/* Call the given callback with the specified button character (from
|
|
|
|
* Unicode Private Use Area) so the text renderer can display it. */
|
|
|
|
|
|
|
|
if (button_value < 0 || button_value > 0xFF)
|
|
|
|
{
|
|
|
|
button_value = 0xFF;
|
|
|
|
}
|
|
|
|
|
2023-02-23 05:14:33 +01:00
|
|
|
UTF8_encoding utf8 = UTF8_encode(0xE000 + button_value);
|
|
|
|
callback(userdata, utf8.bytes, utf8.nbytes);
|
2022-04-30 21:09:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
static inline void call_with_upper(format_callback callback, void* userdata, const char* string, size_t bytes)
|
|
|
|
{
|
|
|
|
/* Call the given callback with the specified string, where the first
|
2023-02-23 05:14:33 +01:00
|
|
|
* letter is changed to uppercase. */
|
|
|
|
if (bytes == 0)
|
2022-04-30 21:09:11 +02:00
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-02-23 05:14:33 +01:00
|
|
|
uint8_t lower_letter_nbytes;
|
|
|
|
uint32_t lower_letter = UTF8_peek_next(string, &lower_letter_nbytes);
|
2022-04-30 21:09:11 +02:00
|
|
|
|
2023-02-23 05:14:33 +01:00
|
|
|
UTF8_encoding upper_letter = UTF8_encode(LOC_toupper_ch(lower_letter));
|
|
|
|
callback(userdata, upper_letter.bytes, upper_letter.nbytes);
|
|
|
|
callback(userdata, &string[lower_letter_nbytes], bytes - lower_letter_nbytes);
|
2022-04-30 21:09:11 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void vformat_cb_valist(
|
|
|
|
format_callback callback,
|
|
|
|
void* userdata,
|
|
|
|
const char* format_string,
|
|
|
|
const char* args_index,
|
|
|
|
va_list args
|
|
|
|
)
|
|
|
|
{
|
|
|
|
/* Variant of vformat_cb which takes a va_list instead of `...`
|
|
|
|
*
|
|
|
|
* Also the core formatting function where everything comes together.
|
|
|
|
* The callback receives each part of the resulting string.
|
|
|
|
* These parts are not null-terminated. */
|
|
|
|
|
|
|
|
const char* cursor = format_string;
|
|
|
|
|
|
|
|
while (true)
|
|
|
|
{
|
|
|
|
/* Fast path: maybe we're at the end? */
|
|
|
|
if (cursor[0] == '\0')
|
|
|
|
{
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Find the next { or }. The } is only needed now for escaping. */
|
|
|
|
const char* first_a = SDL_strchr(cursor, '{');
|
|
|
|
const char* first_b = SDL_strchr(cursor, '}');
|
|
|
|
const char* next_stop = first_a;
|
|
|
|
if (next_stop == NULL || first_b < next_stop)
|
|
|
|
{
|
|
|
|
next_stop = first_b;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (next_stop == NULL)
|
|
|
|
{
|
|
|
|
/* No more placeholders or escapes in this string, run it to the end */
|
|
|
|
callback(userdata, cursor, SDL_strlen(cursor));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Output text until the placeholder... */
|
|
|
|
callback(userdata, cursor, next_stop - cursor);
|
|
|
|
cursor = next_stop;
|
|
|
|
|
|
|
|
if (cursor[0] == '}')
|
|
|
|
{
|
|
|
|
/* Just handle }}, for symmetry with {{ */
|
|
|
|
callback(userdata, cursor, 1);
|
|
|
|
cursor++;
|
|
|
|
if (*cursor == '}')
|
|
|
|
{
|
|
|
|
cursor++;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (cursor[0] == '{' && cursor[1] == '{')
|
|
|
|
{
|
|
|
|
/* Just a {{ */
|
|
|
|
callback(userdata, cursor, 1);
|
|
|
|
cursor += 2;
|
|
|
|
}
|
|
|
|
else if (cursor[0] == '{')
|
|
|
|
{
|
|
|
|
/* Start of a placeholder!
|
|
|
|
* A placeholder can consist of one or more parts separated by |,
|
|
|
|
* the first part always being the key, all others being flags. */
|
|
|
|
|
|
|
|
const char* placeholder_start = cursor;
|
|
|
|
cursor++;
|
|
|
|
const char* name = cursor;
|
|
|
|
size_t name_len = 0;
|
|
|
|
|
|
|
|
bool flag_wordy = false;
|
|
|
|
int flag_digits = 0;
|
|
|
|
bool flag_spaces = false;
|
|
|
|
bool flag_upper = false;
|
|
|
|
|
|
|
|
bool first_iter = true;
|
|
|
|
do
|
|
|
|
{
|
|
|
|
first_a = SDL_strchr(cursor, '|');
|
|
|
|
first_b = SDL_strchr(cursor, '}');
|
|
|
|
next_stop = first_a;
|
|
|
|
if (next_stop == NULL || first_b < next_stop)
|
|
|
|
{
|
|
|
|
next_stop = first_b;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (next_stop == NULL)
|
|
|
|
{
|
|
|
|
/* Unterminated placeholder */
|
|
|
|
callback(userdata, placeholder_start, SDL_strlen(placeholder_start));
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t flag_len = next_stop - cursor;
|
|
|
|
|
|
|
|
if (first_iter)
|
|
|
|
{
|
|
|
|
name_len = flag_len;
|
|
|
|
|
|
|
|
first_iter = false;
|
|
|
|
}
|
|
|
|
else if (flag_len == 5 && SDL_memcmp(cursor, "wordy", 5) == 0)
|
|
|
|
{
|
|
|
|
flag_wordy = true;
|
|
|
|
}
|
|
|
|
else if (flag_len >= 8 && SDL_memcmp(cursor, "digits=", 7) == 0)
|
|
|
|
{
|
|
|
|
/* strtol stops on the first non-digit anyway, so... */
|
|
|
|
flag_digits = SDL_strtol(cursor + 7, NULL, 10);
|
|
|
|
}
|
|
|
|
else if (flag_len == 6 && SDL_memcmp(cursor, "spaces", 6) == 0)
|
|
|
|
{
|
|
|
|
flag_spaces = true;
|
|
|
|
}
|
|
|
|
else if (flag_len == 5 && SDL_memcmp(cursor, "upper", 5) == 0)
|
|
|
|
{
|
|
|
|
flag_upper = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
cursor = next_stop + 1;
|
|
|
|
}
|
|
|
|
while (next_stop[0] != '}');
|
|
|
|
|
|
|
|
/* Now look up the name in the arguments.
|
|
|
|
* The arguments index string is comma-separated,
|
|
|
|
* each argument is name:type. */
|
|
|
|
va_list args_copy;
|
|
|
|
va_copy(args_copy, args);
|
|
|
|
|
|
|
|
const char* args_index_cursor = args_index;
|
|
|
|
|
|
|
|
bool match = false;
|
|
|
|
do
|
|
|
|
{
|
|
|
|
while (args_index_cursor[0] == ' ')
|
|
|
|
{
|
|
|
|
/* Skip spaces between args */
|
|
|
|
args_index_cursor++;
|
|
|
|
}
|
|
|
|
|
|
|
|
const char* next_comma = SDL_strchr(args_index_cursor, ',');
|
|
|
|
const char* next_colon = SDL_strchr(args_index_cursor, ':');
|
|
|
|
if (next_comma == NULL)
|
|
|
|
{
|
|
|
|
next_comma = SDL_strchr(args_index_cursor, '\0');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (next_colon == NULL || next_colon > next_comma)
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const char* arg_name = args_index_cursor;
|
|
|
|
size_t arg_name_len = next_colon - arg_name;
|
|
|
|
const char* arg_type = next_colon + 1;
|
|
|
|
size_t arg_type_len = next_comma - arg_type;
|
|
|
|
|
|
|
|
trim_whitespace(&arg_name, &arg_name_len);
|
|
|
|
trim_whitespace(&arg_type, &arg_type_len);
|
|
|
|
|
2022-12-29 05:02:09 +01:00
|
|
|
match = (arg_name_len == name_len && SDL_memcmp(arg_name, name, name_len) == 0)
|
|
|
|
|| (arg_name_len == 1 && arg_name[0] == '_');
|
2022-04-30 21:09:11 +02:00
|
|
|
|
|
|
|
if (arg_type_len == 3 && SDL_memcmp(arg_type, "int", 3) == 0)
|
|
|
|
{
|
|
|
|
int value = va_arg(args_copy, int);
|
|
|
|
|
|
|
|
if (match)
|
|
|
|
{
|
|
|
|
if (flag_wordy)
|
|
|
|
{
|
|
|
|
char* number = HELP_number_words(value);
|
|
|
|
if (flag_upper)
|
|
|
|
{
|
|
|
|
call_with_upper(callback, userdata, number, SDL_strlen(number));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
callback(userdata, number, SDL_strlen(number));
|
|
|
|
}
|
2022-12-01 07:30:16 +01:00
|
|
|
VVV_free(number);
|
2022-04-30 21:09:11 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
const char* format = flag_spaces ? "%*d" : "%0*d";
|
|
|
|
char buffer[24];
|
|
|
|
SDL_snprintf(buffer, sizeof(buffer), format, flag_digits, value);
|
|
|
|
callback(userdata, buffer, SDL_strlen(buffer));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (arg_type_len == 3 && SDL_memcmp(arg_type, "str", 3) == 0)
|
|
|
|
{
|
|
|
|
const char* value = va_arg(args_copy, const char*);
|
|
|
|
|
|
|
|
if (match)
|
|
|
|
{
|
|
|
|
if (value == NULL)
|
|
|
|
{
|
|
|
|
callback(userdata, "[null]", 6);
|
|
|
|
}
|
|
|
|
else if (flag_upper)
|
|
|
|
{
|
|
|
|
call_with_upper(callback, userdata, value, SDL_strlen(value));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
callback(userdata, value, SDL_strlen(value));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else if (arg_type_len == 3 && SDL_memcmp(arg_type, "but", 3) == 0)
|
|
|
|
{
|
|
|
|
int button_value = va_arg(args_copy, int);
|
|
|
|
|
|
|
|
if (match)
|
|
|
|
{
|
|
|
|
call_with_button(callback, userdata, button_value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
/* Unknown type, now what type do we give va_arg? */
|
|
|
|
match = false;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (match)
|
|
|
|
{
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
/* Remember: next_comma can also be the final '\0' */
|
|
|
|
args_index_cursor = next_comma + 1;
|
|
|
|
}
|
|
|
|
while (args_index_cursor[-1] != '\0');
|
|
|
|
|
|
|
|
va_end(args_copy);
|
|
|
|
|
|
|
|
if (!match)
|
|
|
|
{
|
|
|
|
callback(userdata, "[", 1);
|
|
|
|
callback(userdata, name, name_len);
|
|
|
|
callback(userdata, "?]", 2);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
void vformat_cb(
|
|
|
|
format_callback callback,
|
|
|
|
void* userdata,
|
|
|
|
const char* format_string,
|
|
|
|
const char* args_index,
|
|
|
|
...
|
|
|
|
)
|
|
|
|
{
|
|
|
|
/* Format with a user-supplied callback.
|
|
|
|
* The callback receives each part of the resulting string.
|
|
|
|
* These parts are not null-terminated. */
|
|
|
|
|
|
|
|
va_list args;
|
|
|
|
va_start(args, args_index);
|
|
|
|
|
|
|
|
vformat_cb_valist(callback, userdata, format_string, args_index, args);
|
|
|
|
|
|
|
|
va_end(args);
|
|
|
|
}
|
|
|
|
|
|
|
|
typedef struct _buffer_info
|
|
|
|
{
|
|
|
|
size_t total_needed;
|
|
|
|
char* buffer_cursor;
|
|
|
|
size_t buffer_left;
|
|
|
|
} buffer_info;
|
|
|
|
|
|
|
|
static void callback_buffer_append(void* userdata, const char* string, size_t bytes)
|
|
|
|
{
|
|
|
|
/* Callback for vformat_buf. */
|
|
|
|
|
|
|
|
buffer_info* buf = (buffer_info*) userdata;
|
|
|
|
buf->total_needed += bytes;
|
|
|
|
|
|
|
|
if (buf->buffer_cursor != NULL && buf->buffer_left > 0)
|
|
|
|
{
|
|
|
|
size_t copy_len = SDL_min(bytes, buf->buffer_left);
|
|
|
|
|
|
|
|
SDL_memcpy(buf->buffer_cursor, string, copy_len);
|
|
|
|
|
|
|
|
buf->buffer_cursor += copy_len;
|
|
|
|
buf->buffer_left -= copy_len;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t vformat_buf_valist(
|
|
|
|
char* buffer,
|
|
|
|
size_t buffer_len,
|
|
|
|
const char* format_string,
|
|
|
|
const char* args_index,
|
|
|
|
va_list args
|
|
|
|
)
|
|
|
|
{
|
|
|
|
/* Variant of vformat_buf which takes a va_list instead of `...` */
|
|
|
|
|
|
|
|
buffer_info buf = {
|
|
|
|
.total_needed = 1,
|
|
|
|
.buffer_cursor = buffer,
|
|
|
|
.buffer_left = buffer_len
|
|
|
|
};
|
|
|
|
|
|
|
|
if (buf.buffer_left != 0)
|
|
|
|
{
|
|
|
|
/* We do still need to write the null terminator */
|
|
|
|
buf.buffer_left--;
|
|
|
|
}
|
|
|
|
|
|
|
|
vformat_cb_valist(callback_buffer_append, (void*) &buf, format_string, args_index, args);
|
|
|
|
if (buffer != NULL && buffer_len > 0)
|
|
|
|
{
|
|
|
|
*buf.buffer_cursor = '\0';
|
|
|
|
}
|
|
|
|
|
|
|
|
return buf.total_needed;
|
|
|
|
}
|
|
|
|
|
|
|
|
size_t vformat_buf(
|
|
|
|
char* buffer,
|
|
|
|
size_t buffer_len,
|
|
|
|
const char* format_string,
|
|
|
|
const char* args_index,
|
|
|
|
...
|
|
|
|
)
|
|
|
|
{
|
|
|
|
/* Format to the specified buffer.
|
|
|
|
* Ensures buffer is not overrun, and that it is null-terminated.
|
|
|
|
* Returns total number of bytes that were needed, or that would have been needed.
|
|
|
|
* You may pass a NULL buffer and/or size 0 if you only need this needed length. */
|
|
|
|
|
|
|
|
va_list args;
|
|
|
|
va_start(args, args_index);
|
|
|
|
|
|
|
|
size_t total_needed = vformat_buf_valist(buffer, buffer_len, format_string, args_index, args);
|
|
|
|
|
|
|
|
va_end(args);
|
|
|
|
|
|
|
|
return total_needed;
|
|
|
|
}
|
|
|
|
|
|
|
|
char* vformat_alloc_valist(
|
|
|
|
const char* format_string,
|
|
|
|
const char* args_index,
|
|
|
|
va_list args
|
|
|
|
)
|
|
|
|
{
|
|
|
|
/* Variant of vformat_alloc which takes a va_list instead of `...` */
|
|
|
|
|
|
|
|
size_t needed = vformat_buf_valist(NULL, 0, format_string, args_index, args);
|
|
|
|
char* buffer = SDL_malloc(needed);
|
|
|
|
if (buffer == NULL)
|
|
|
|
{
|
|
|
|
return NULL;
|
|
|
|
}
|
|
|
|
vformat_buf_valist(buffer, needed, format_string, args_index, args);
|
|
|
|
|
|
|
|
return buffer;
|
|
|
|
}
|
|
|
|
|
|
|
|
char* vformat_alloc(
|
|
|
|
const char* format_string,
|
|
|
|
const char* args_index,
|
|
|
|
...
|
|
|
|
)
|
|
|
|
{
|
|
|
|
/* Format to an automatically allocated and resized buffer.
|
2022-12-01 07:30:16 +01:00
|
|
|
* Caller must VVV_free. */
|
2022-04-30 21:09:11 +02:00
|
|
|
|
|
|
|
va_list args;
|
|
|
|
va_start(args, args_index);
|
|
|
|
|
|
|
|
char* buffer = vformat_alloc_valist(format_string, args_index, args);
|
|
|
|
|
|
|
|
va_end(args);
|
|
|
|
|
|
|
|
return buffer;
|
|
|
|
}
|