pebble/src/fw/applib/template_string.c
2025-01-27 11:38:16 -08:00

869 lines
28 KiB
C

/*
* Copyright 2024 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "template_string.h"
#include "template_string_private.h"
#include "services/common/i18n/i18n.h"
#include "syscall/syscall.h"
#include "system/passert.h"
#include "util/attributes.h"
#include "util/math.h"
#include "util/size.h"
#include "util/string.h"
#include <ctype.h>
#include <limits.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#define MAX_FILTER_NAME_LENGTH 16
#define SUPPORT_MONTH 0
#define SUPPORT_YEAR 0
typedef enum {
PredicateCondition_Invalid,
// This order is important; >= must be next after >, and <= must be next after <.
// This is so that the parser can just add 1 to the enum value when it finds the = character.
PredicateCondition_L,
PredicateCondition_LE,
PredicateCondition_G,
PredicateCondition_GE,
} PredicateCondition;
typedef enum {
FormatUnits_None,
FormatUnits_Abbreviated,
FormatUnits_Full,
} FormatUnits;
typedef struct {
const char name[MAX_FILTER_NAME_LENGTH];
// Filters must manually advance state->position to the parenthesis that indicates the end of
// the filter arguments.
void (*cb)(TemplateStringState *state);
} FilterImplementation;
static void prv_filter_format(TemplateStringState *state);
static void prv_filter_time_until(TemplateStringState *state);
static void prv_filter_time_since(TemplateStringState *state);
static void prv_filter_end(TemplateStringState *state);
static const FilterImplementation s_filter_impls[] = {
{ "format", prv_filter_format, },
{ "time_until", prv_filter_time_until, },
{ "time_since", prv_filter_time_since, },
{ "end", prv_filter_end, },
};
static void prv_handle_escape_character(TemplateStringState *state) {
if (*state->position == '\\') {
state->position++;
if (*state->position == '\0') {
state->error->status = TemplateStringErrorStatus_InvalidEscapeCharacter;
}
}
}
static bool prv_predicate_check(char ch) {
return ((ch == '>') || (ch == '<'));
}
static bool prv_format_string_ending(char ch) {
return ((ch == ',') || (ch == ')'));
}
static bool prv_predicate_valid_splitter(char ch) {
return ((ch == ':') || prv_format_string_ending(ch));
}
T_STATIC intmax_t prv_template_predicate_time(TemplateStringState *state) {
bool negative = false;
if (*state->position == '-') {
negative = true;
state->position++;
}
bool total_value_valid = false;
intmax_t total_value = 0;
while (!total_value_valid || !prv_predicate_valid_splitter(*state->position)) {
const char *endpos;
int value = strtol(state->position, (char **)&endpos, 10);
if (endpos == state->position) {
state->error->status = TemplateStringErrorStatus_InvalidTimeUnit;
return 0;
}
state->position = endpos;
int multiplier = 1;
switch (*state->position) {
// NOTE: This number of seconds is a hack! See PBL-39903
#if SUPPORT_YEAR
case 'y': multiplier = 365 * SECONDS_PER_DAY; break;
#endif
// NOTE: This number of seconds is a hack! See PBL-39903
#if SUPPORT_MONTH
case 'm': multiplier = 30 * SECONDS_PER_DAY; break;
#endif
case 'd': multiplier = SECONDS_PER_DAY; break;
case 'H': multiplier = SECONDS_PER_HOUR; break;
case 'M': multiplier = SECONDS_PER_MINUTE; break;
case 'S': multiplier = 1; break;
default:
state->error->status = TemplateStringErrorStatus_InvalidTimeUnit;
return 0;
}
state->position++;
total_value += value * multiplier;
total_value_valid = true;
}
if (negative) {
total_value = -total_value;
}
return total_value;
}
T_STATIC bool prv_template_predicate_match(TemplateStringState *state, PredicateCondition *cond,
intmax_t *value) {
*cond = PredicateCondition_Invalid;
if (*state->position == '<') {
*cond = PredicateCondition_L;
} else if (*state->position == '>') {
*cond = PredicateCondition_G;
} else {
WTF;
}
state->position++;
if (*state->position == '=') {
(*cond)++;
state->position++;
} else if (!isdigit(*state->position)) {
state->error->status = TemplateStringErrorStatus_InvalidTimeUnit;
return false;
}
*value = prv_template_predicate_time(state);
if (state->error->status != TemplateStringErrorStatus_Success) {
return false;
}
switch (*cond) {
case PredicateCondition_G:
return (state->filter_state > *value);
case PredicateCondition_GE:
return (state->filter_state >= *value);
case PredicateCondition_L:
return (state->filter_state < *value);
case PredicateCondition_LE:
return (state->filter_state <= *value);
default:
WTF;
}
}
static const char * const s_Tstrings[3][3] = {
{ "H", "M", "S", },
{ "aH", "aM", "aS", },
{ "uH", "uM", "uS", },
};
static const char * const s_splitters[3][2] = {
/// The first separator in `<hour>:<minute>:<second>`
{ i18n_ctx_noop("TmplStringSep", ":"),
/// The second separator in `<hour>:<minute>:<second>`
i18n_ctx_noop("TmplStringSep", ":"), },
/// The first separator in `<hour> hr <minute> min <second> sec`
{ i18n_ctx_noop("TmplStringSep", " "),
/// The second separator in `<hour> hr <minute> min <second> sec`
i18n_ctx_noop("TmplStringSep", " "), },
/// The first separator in `<hour> hours, <minute> minutes, and <second> seconds`
{ i18n_ctx_noop("TmplStringSep", ", "),
/// The second separator in `<hour> hours, <minute> minutes, and <second> seconds`
i18n_ctx_noop("TmplStringSep", ", and "), },
};
/*
Flag truth table.
fmt = >= 1 hour >= 1 minute other
%T = %H:%0M:%0S %M:%0S %S
%uT = %uH, %uM, and %uS %uM, and %uS %uS
%aT = %aH %aM %aS %aM %aS %aS
%0T = %0H:%0M:%0S %0M:%0S %0S
%fT = %fH:%0M:%0S %fM:%0S %fS
%0uT = %0uH, %0uM, and %0uS %0uM, and %0uS %0uS
%0aT = %0aH %0aM %0aS %0aM %0aS %0aS
%0fT = %0fH:%0M:%0S %0fM:%0S %0fS
%fuT = %fuH, %uM, and %uS %fuM, and %uS %fuS
%faT = %faH %aM %aS %faM %aS %faS
%0fuT = %f0uH, %0uM, and %0uS %f0uM, and %0uS %f0uS
%0faT = %f0aH %0aM %0aS %f0aM %0aS %f0aS
*/
/*
0 flag adds 0 flag to all sub-specs
f flag adds f flag to first sub-spec
ua. are unique
*/
static void prv_append_string_i18n(TemplateStringState *state, const char *str) {
size_t len = sys_i18n_get_length(str);
if (state->output_remaining < len) {
len = state->output_remaining;
}
if (!len) {
return;
}
// len needs +1 in order for i18n_get_with_buffer to write all the characters we want.
// It's ok that it writes the NUL at the end because we've already reserved that space.
sys_i18n_get_with_buffer(str, state->output, len + 1);
state->output += len;
state->output_remaining -= len;
}
static void prv_append_number(TemplateStringState *state, const char *fmt, int value) {
size_t len = snprintf(NULL, 0, fmt, value);
if (state->output_remaining < len) {
len = state->output_remaining;
}
if (!len) {
return;
}
// len needs +1 in order for snprintf to write all the characters we want.
// It's ok that it writes the NUL at the end because we've already reserved that space.
snprintf(state->output, len + 1, fmt, value);
state->output += len;
state->output_remaining -= len;
}
static void prv_append_char(TemplateStringState *state, char c) {
if (state->output_remaining >= 1) {
*state->output++ = c;
state->output_remaining--;
}
}
static const char * const s_second_strings[3][2] = {
/// Singular suffix for seconds with no units
{ i18n_ctx_noop("TmplStringSing", ""),
/// Plural suffix for seconds with no units
i18n_ctx_noop("TmplStringPlur", "")},
/// Singular suffix for seconds with abbreviated units
{ i18n_ctx_noop("TmplStringSing", " sec"),
/// Plural suffix for seconds with abbreviated units
i18n_ctx_noop("TmplStringPlur", " sec")},
/// Singular suffix for seconds with full units
{ i18n_ctx_noop("TmplStringSing", " second"),
/// Plural suffix for seconds with full units
i18n_ctx_noop("TmplStringPlur", " seconds")},
};
static const char * const s_minute_strings[3][2] = {
/// Singular suffix for minutes with no units
{ i18n_ctx_noop("TmplStringSing", ""),
/// Plural suffix for minutes with no units
i18n_ctx_noop("TmplStringPlur", "")},
/// Singular suffix for minutes with abbreviated units
{ i18n_ctx_noop("TmplStringSing", " min"),
/// Plural suffix for minutes with abbreviated units
i18n_ctx_noop("TmplStringPlur", " min")},
/// Singular suffix for minutes with full units
{ i18n_ctx_noop("TmplStringSing", " minute"),
/// Plural suffix for minutes with full units
i18n_ctx_noop("TmplStringPlur", " minutes")},
};
static const char * const s_hour_strings[3][2] = {
/// Singular suffix for hours with no units
{ i18n_ctx_noop("TmplStringSing", ""),
/// Plural suffix for hours with no units
i18n_ctx_noop("TmplStringPlur", "")},
/// Singular suffix for hours with abbreviated units
{ i18n_ctx_noop("TmplStringSing", " hr"),
/// Plural suffix for hours with abbreviated units
i18n_ctx_noop("TmplStringPlur", " hr")},
/// Singular suffix for hours with full units
{ i18n_ctx_noop("TmplStringSing", " hour"),
/// Plural suffix for hours with full units
i18n_ctx_noop("TmplStringPlur", " hours")},
};
static const char * const s_day_strings[3][2] = {
/// Singular suffix for days with no units
{ i18n_ctx_noop("TmplStringSing", ""),
/// Plural suffix for days with no units
i18n_ctx_noop("TmplStringPlur", "")},
/// Singular suffix for days with abbreviated units
{ i18n_ctx_noop("TmplStringSing", " d"),
/// Plural suffix for days with abbreviated units
i18n_ctx_noop("TmplStringPlur", " d")},
/// Singular suffix for days with full units
{ i18n_ctx_noop("TmplStringSing", " day"),
/// Plural suffix for days with full units
i18n_ctx_noop("TmplStringPlur", " days")},
};
#if SUPPORT_MONTH
static const char * const s_month_strings[3][2] = {
/// Singular suffix for months with no units
{ i18n_ctx_noop("TmplStringSing", ""),
/// Plural suffix for months with no units
i18n_ctx_noop("TmplStringPlur", "")},
/// Singular suffix for months with abbreviated units
{ i18n_ctx_noop("TmplStringSing", " mo"),
/// Plural suffix for months with abbreviated units
i18n_ctx_noop("TmplStringPlur", " mo")},
/// Singular suffix for months with full units
{ i18n_ctx_noop("TmplStringSing", " month"),
/// Plural suffix for months with full units
i18n_ctx_noop("TmplStringPlur", " months")},
};
#endif
#if SUPPORT_YEAR
static const char * const s_year_strings[3][2] = {
/// Singular suffix for years with no units
{ i18n_ctx_noop("TmplStringSing", ""),
/// Plural suffix for years with no units
i18n_ctx_noop("TmplStringPlur", "")},
/// Singular suffix for years with abbreviated units
{ i18n_ctx_noop("TmplStringSing", " yr"),
/// Plural suffix for years with abbreviated units
i18n_ctx_noop("TmplStringPlur", " yr")},
/// Singular suffix for years with full units
{ i18n_ctx_noop("TmplStringSing", " year"),
/// Plural suffix for years with full units
i18n_ctx_noop("TmplStringPlur", " years")},
};
#endif
static void prv_do_conversion(TemplateStringState *state, intmax_t value, int divide, int mod,
const char * const suffix_strings[3][2], FormatUnits add_units,
bool zero_pad, bool should_mod) {
int remain = (value % divide);
if (!state->time_was_until) {
// We want to go in reverse for 'since'
remain = divide - remain;
} else {
// Add 1 because the next eval time is how long until the result changes.
remain++;
}
if (state->eval_cond) {
if (remain < state->eval_cond->eval_time) {
state->eval_cond->eval_time = remain;
}
}
value /= divide;
if (should_mod && (mod != 0)) {
value %= mod;
}
prv_append_number(state, zero_pad ? "%02d" : "%d", value);
prv_append_string_i18n(state, suffix_strings[add_units][value != 1]);
}
// This is a recursive function, so watch out!
// The recursion happens on the %R and %T cases only, and will only recurse once.
// So when adding stack variables, realize the stack usage may be doubled!
T_STATIC const char *prv_template_format_specifier(TemplateStringState *state, const char *input,
intmax_t value) {
if (*input == '%') { // Escaped %
prv_append_char(state, *input);
input++;
return input;
}
FormatUnits add_units = FormatUnits_None;
bool zero_pad = false;
bool modulus = true;
bool checking_flags = true;
while (checking_flags) {
switch (*input) {
case 'a':
add_units = FormatUnits_Abbreviated;
break;
case 'u':
add_units = FormatUnits_Full;
break;
case '-':
value = -value;
break;
case '0':
zero_pad = true;
break;
case 'f':
modulus = false;
break;
default:
checking_flags = false;
break;
}
if (checking_flags) {
input++;
}
}
if (value < 0) {
prv_append_char(state, '-');
value = -value;
}
int macro_units = 2;
if (value >= SECONDS_PER_MINUTE) {
macro_units--;
}
if (value >= SECONDS_PER_HOUR) {
macro_units--;
}
int macro_end = 3;
// conversion specifiers
switch (*input) {
#if SUPPORT_YEAR
case 'y': // year
// NOTE: This number of seconds to divide by is a hack! See PBL-39903
prv_do_conversion(state, value, 365 * SECONDS_PER_DAY, 100, s_year_strings,
add_units, zero_pad, modulus);
break;
#endif
#if SUPPORT_MONTH
case 'm': // month
// NOTE: This number of seconds to divide by is a hack! See PBL-39903
prv_do_conversion(state, value, 30 * SECONDS_PER_DAY, 12, s_month_strings,
add_units, zero_pad, modulus);
break;
#endif
case 'd': // day
// NOTE: This number of modulus is a hack! See PBL-39903
#if SUPPORT_MONTH
prv_do_conversion(state, value, SECONDS_PER_DAY, 30, s_day_strings,
add_units, zero_pad, modulus);
#else
prv_do_conversion(state, value, SECONDS_PER_DAY, 0, s_day_strings,
add_units, zero_pad, modulus);
#endif
break;
case 'H': // hour
prv_do_conversion(state, value, SECONDS_PER_HOUR, HOURS_PER_DAY, s_hour_strings,
add_units, zero_pad, modulus);
break;
case 'M': // minute
prv_do_conversion(state, value, SECONDS_PER_MINUTE, MINUTES_PER_HOUR, s_minute_strings,
add_units, zero_pad, modulus);
break;
case 'S': // second
prv_do_conversion(state, value, 1, SECONDS_PER_MINUTE, s_second_strings,
add_units, zero_pad, modulus);
break;
case 'R': // H:M
// R is mostly the same as T, just without seconds.
macro_end--;
// fall-thru
case 'T': { // H:M:S
char macro_spec[16];
// Always show the last unit, even if it's 0.
macro_units = MIN(macro_units, macro_end - 1);
for (int i = macro_units; i < macro_end; i++) {
char *macro_ptr = macro_spec;
if (zero_pad || ((i != macro_units) && (add_units == FormatUnits_None))) {
*macro_ptr++ = '0';
}
if (!modulus && (i == macro_units)) {
*macro_ptr++ = 'f';
}
strcpy(macro_ptr, s_Tstrings[add_units][i]);
macro_ptr += strlen(s_Tstrings[add_units][i]);
*macro_ptr = '\0';
prv_template_format_specifier(state, macro_spec, value);
if (i != macro_end - 1) {
prv_append_string_i18n(state, s_splitters[add_units][i >= macro_end - 2]);
}
}
break;
}
default:
state->error->status = TemplateStringErrorStatus_InvalidConversionSpecifier;
return input;
}
// Skip the conversion specifier.
input++;
return input;
}
static bool prv_format_predicate(TemplateStringState *state, bool previously_matched) {
bool match = true;
if (!prv_predicate_check(*state->position)) {
return match;
}
PredicateCondition predicate_cond;
intmax_t predicate_value = 0;
// If this is a predicate, we need to evaluate it for a match.
match = prv_template_predicate_match(state, &predicate_cond, &predicate_value);
if (state->error->status != TemplateStringErrorStatus_Success) {
return false;
}
// Predicate matcher will only leave on :,) or error, so this should never trip.
if (!prv_predicate_valid_splitter(*state->position)) {
WTF;
}
int wait_time;
// Need to handle predicates differently based on whether the value is incrementing or
// decrementing over time.
PredicateCondition cond_to_expire;
PredicateCondition cond_to_valid;
if (!state->time_was_until) {
// Value increments over time, so a < will expire and a > will become valid.
cond_to_expire = PredicateCondition_L;
cond_to_valid = PredicateCondition_G;
wait_time = predicate_value - state->filter_state;
} else {
// Value decrements over time, so a < will become valid and a > will expire.
cond_to_expire = PredicateCondition_G;
cond_to_valid = PredicateCondition_L;
wait_time = state->filter_state - predicate_value;
}
if (!previously_matched && match && ((predicate_cond == cond_to_expire) ||
(predicate_cond == cond_to_expire + 1))) {
// This predicate could expire over time.
// If the conditional is equal, add 1 to the wait time, because the equals case stays
// valid on the specified value.
if (predicate_cond == cond_to_expire + 1) {
wait_time++;
}
} else if (!match && ((predicate_cond == cond_to_valid) ||
(predicate_cond == cond_to_valid + 1))) {
// This predicate could become valid over time.
// If the conditional is not equal, add 1 to the wait time, because only the equals case
// becomes valid on the specified value.
if (predicate_cond == cond_to_valid) {
wait_time++;
}
} else {
wait_time = INT_MAX;
}
if (state->eval_cond) {
if (wait_time < state->eval_cond->eval_time) {
state->eval_cond->eval_time = wait_time;
}
}
// Only characters possible here are :,)
if (*state->position == ':') {
state->position++;
}
return match;
}
static void prv_format_process_format_string(TemplateStringState *state, char delimiter) {
// Predicate matched (or is default case), so let's parse the string.
while ((*state->position != delimiter) && (*state->position != '\0')) {
if (*state->position != '%') { // Not a format character
prv_handle_escape_character(state);
if (state->error->status != TemplateStringErrorStatus_Success) {
return;
}
prv_append_char(state, *state->position);
state->position++;
} else {
// Skip over the %
state->position++;
state->position = prv_template_format_specifier(state, state->position,
state->filter_state);
if (state->error->status != TemplateStringErrorStatus_Success) {
return;
}
}
}
if (*state->position == '\0') {
state->error->status = TemplateStringErrorStatus_MissingClosingQuote;
return;
}
// Skip the delimiter
state->position++;
}
static void prv_format_skip_format_string(TemplateStringState *state, char delimiter) {
// No match, so let's move along to the next one.
while ((*state->position != delimiter) && (*state->position != '\0')) {
prv_handle_escape_character(state);
if (state->error->status != TemplateStringErrorStatus_Success) {
return;
}
state->position++;
}
if (*state->position == '\0') {
state->error->status = TemplateStringErrorStatus_MissingClosingQuote;
return;
}
// Skip the delimiter
state->position++;
}
static void prv_filter_format(TemplateStringState *state) {
bool match;
bool previously_matched = false;
bool did_output = false;
// We need to iterate all the way through for finding the proper 'next' time.
while (*state->position != ')') {
match = prv_format_predicate(state, previously_matched);
if (state->error->status != TemplateStringErrorStatus_Success) {
return;
}
// A force-default case
if (prv_format_string_ending(*state->position)) {
state->position++;
continue;
}
// Get the delimiter being used.
const char delimiter = *state->position;
if ((delimiter != '\'') && (delimiter != '"')) {
state->error->status = TemplateStringErrorStatus_MissingOpeningQuote;
return;
}
state->position++;
if (match && !previously_matched) {
prv_format_process_format_string(state, delimiter);
did_output = true;
previously_matched = true;
} else {
prv_format_skip_format_string(state, delimiter);
}
if (state->error->status != TemplateStringErrorStatus_Success) {
return;
}
if (!prv_format_string_ending(*state->position)) {
state->error->status = TemplateStringErrorStatus_InvalidArgumentSeparator;
return;
} else if (*state->position == ',') {
state->position++;
if (*state->position == ')') {
state->error->status = TemplateStringErrorStatus_MissingArgument;
return;
}
}
}
if (!did_output) {
// If no output was generated, it's an error.
state->error->status = TemplateStringErrorStatus_CantResolve;
}
// format() must be the last filter, and ends the sequence.
state->filters_complete = true;
}
static void prv_filter_time_until(TemplateStringState *state) {
char *endptr;
time_t target_time = strtol(state->position, &endptr, 10);
if (*endptr != ')') {
state->error->status = TemplateStringErrorStatus_MissingClosingParen;
return;
}
state->position = endptr;
state->filter_state = target_time - state->vars->current_time;
state->time_was_until = true;
}
static void prv_filter_time_since(TemplateStringState *state) {
prv_filter_time_until(state);
if (state->error->status != TemplateStringErrorStatus_Success) {
return;
}
state->filter_state = -state->filter_state;
state->time_was_until = false;
}
static void prv_filter_end(TemplateStringState *state) {
state->filters_complete = true;
}
T_STATIC void prv_template_evaluate_filter(TemplateStringState *state, const char *filter_name,
const char *parameters_start) {
for (size_t i = 0; i < ARRAY_LENGTH(s_filter_impls); i++) {
if (strcmp(s_filter_impls[i].name, filter_name) == 0) {
state->position = parameters_start;
s_filter_impls[i].cb(state);
return;
}
}
state->error->status = TemplateStringErrorStatus_UnknownFilter;
}
static void prv_template_eval(TemplateStringState *state) {
while ((*state->position != '}') && (*state->position != '\0')) {
if (state->filters_complete) {
state->error->status = TemplateStringErrorStatus_FormatBeforeLast;
return;
}
// Find the filter's opening paren
const char *filter_name_paren = strchr(state->position, '(');
if (!filter_name_paren) {
state->error->status = TemplateStringErrorStatus_MissingOpeningParen;
return;
}
// Copy out the filter name
char filter_name[MAX_FILTER_NAME_LENGTH];
size_t len = MIN(MAX_FILTER_NAME_LENGTH - 1, filter_name_paren - state->position);
strncpy(filter_name, state->position, len);
filter_name[len] = '\0';
prv_template_evaluate_filter(state, filter_name, filter_name_paren + 1);
if (state->error->status != TemplateStringErrorStatus_Success) {
return;
}
if (*state->position != ')') {
state->error->status = TemplateStringErrorStatus_MissingClosingParen;
return;
}
// Advance pointer to the character after the filter
state->position++;
if (*state->position == '|') {
state->position++;
continue;
} else if (*state->position == '}') {
continue;
} else {
state->error->status = TemplateStringErrorStatus_MissingClosingBrace;
return;
}
}
// Must end on a closing brace.
if (*state->position != '}') {
state->error->status = TemplateStringErrorStatus_MissingClosingBrace;
return;
}
// Did not generate an output.
if (!state->filters_complete) {
state->error->status = TemplateStringErrorStatus_NoResultGenerated;
return;
}
// Skip past the closing brace.
state->position++;
}
bool template_string_evaluate(const char *input_template_string, char *output, size_t output_size,
TemplateStringEvalConditions *eval_cond,
const TemplateStringVars *vars, TemplateStringError *error) {
TemplateStringState state = {
.position = input_template_string,
.output = output,
.output_remaining = output_size,
.eval_cond = eval_cond,
.vars = vars,
.error = error,
.time_was_until = false,
.filter_state = 0,
};
if (!state.position || !state.vars || !state.error) {
if (state.error) {
state.error->status = TemplateStringErrorStatus_InvalidParameter;
}
return false;
}
// We have no output space, so don't bother trying to write anything.
// By unifying these states, we can just check `output_remaining` against zero for writing.
if (!state.output || !state.output_remaining) {
state.output = NULL;
state.output_remaining = 0;
} else {
// Subtract 1 for the null terminator.
state.output_remaining--;
}
if (state.eval_cond) {
state.eval_cond->eval_time = INT_MAX;
state.eval_cond->force_eval_on_time = false;
}
state.error->status = TemplateStringErrorStatus_Success;
while (*state.position != '\0') {
if (*state.position != '{') {
prv_handle_escape_character(&state);
if (state.error->status != TemplateStringErrorStatus_Success) {
break;
}
prv_append_char(&state, *state.position);
state.position++;
} else { // Template
state.position++;
prv_template_eval(&state);
if (state.error->status != TemplateStringErrorStatus_Success) {
break;
}
state.time_was_until = false;
state.filter_state = 0;
state.filters_complete = false;
}
}
if (state.error->status != TemplateStringErrorStatus_Success) {
// get the position index.
state.error->index_in_string = state.position - input_template_string;
}
// Null terminator
if (state.output) {
*state.output = '\0';
}
if (state.eval_cond) {
// Adjust eval_time if it never got set.
if (state.eval_cond->eval_time == INT_MAX) {
// If we never set the re-evaluation time, set `eval_time` to 0.
// This is the value we specified for "we don't need to re-evaluate".
state.eval_cond->eval_time = 0;
} else {
// `eval_time` is an absolute timestamp, so add the input time to the relative time offset.
state.eval_cond->eval_time += state.vars->current_time;
state.eval_cond->force_eval_on_time = true;
}
}
return (state.error->status == TemplateStringErrorStatus_Success);
}